<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>skyjake</title>
<link href="gemini://skyjake.fi/gemlog/" />
<link rel="self" type="application/atom+xml" href="gemini://skyjake.fi/gemlog/atom.xml" />
<id>gemini://skyjake.fi/gemlog/</id>
<updated>2026-11-07T21:21:54Z</updated>
<generator uri="gemini://skyjake.fi/gemlog/" version="1.0">gmi-hatch</generator>
<entry>
    <title>Lagrange 2</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/11/lagrange-2.0.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/11/lagrange-2.0.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-11_lagrange-2.0.gmi</id>
    <published>2026-11-06T21:21:54Z</published>
    <updated>2026-11-07T21:21:54Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p class="lede">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.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/lagrange/docs/lagrange-2.gpub">This post is also available as a Gempub book: &quot;Lagrange 2&quot; (530 KB)</a></div>
<p></p>
<div class="link web-link"><span class="link-icon">🌐</span><a href="https://codeberg.org/skyjake/lagrange/releases/tag/v2.0.0">Download Lagrange v2.0</a></div>
<p></p>
<h2 id="turning-a-new-page">Turning a New Page</h2>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="upgrading-and-downgrading">⚠ Upgrading (and downgrading)</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h2 id="look-and-feel">Look and Feel</h2>
<p></p>
<p>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.</p>
<p></p>
<h3 id="rounded-all-around">Rounded all-around</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<img src="/gemlog/images/v2.0_ctx.png" title="Context menu with rounded corners (PNG, 32 KB)">
<p></p>
<h3 id="system-fonts">System fonts</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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:</p>
<p></p>
<ul>
<li>On Apple operating systems (macOS, iOS/iPadOS), the Core Text framework is used to manage fonts and both shape and render text natively. This includes access to SF Symbols for UI icons, with system color Emoji, achieving text rendering on par with actual native apps.</li>
<li>On Windows, Linux, and Unix platforms, FreeType is used for rendering glyphs and managing fonts, while HarfBuzz still shapes the text and FriBidi handles the bidirectional transforms. You can access all system fonts, including both vector and bitmap-based Emoji fonts.</li>
</ul>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="sliding-with-precision">Sliding with precision</h3>
<p></p>
<p>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.</p>
<p></p>
<p>Version 2 adds a new widget class for sliders. These are used particularly in Preferences/Settings for entering numeric values.</p>
<p></p>
<img src="/gemlog/images/v2.0_slider.png" title="Sliders in the Preferences dialog (PNG, 11 KB)">
<p></p>
<h3 id="document-themed-tab-bar">Document-themed tab bar</h3>
<p></p>
<p>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.</p>
<p></p>
<h3 id="preferences">Preferences</h3>
<p></p>
<p>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.</p>
<p></p>
<ul>
<li>&quot;General&quot; has the UI layout settings like nav/tab bar positions, tab bar hiding, input prompt position, split view pinning, and window state restoring.</li>
<li>&quot;Appearance&quot; is for choosing UI colors, UI scaling, UI font, and other visual UI options.</li>
<li>&quot;Behavior&quot; has the feed refresh interval, mouse hover and return key behavior, and animation and scrolling options.</li>
<li>Document theming is configured on the &quot;Fonts&quot;, &quot;Colors&quot;, &quot;Page Style&quot;, and &quot;Layout&quot; pages. Note the &quot;Applies to:&quot; dropdown at the top. Depending on the chosen value, the settings apply to all protocols or only a particular one. This enables fine-grained content theming per protocol. For example, Gopher defaults to Monospace body.</li>
<li>Key bindings have been grouped under subheadings for easier navigation, and there is a filter field to narrow down the list.</li>
<li>The Preferences window&apos;s height can be changed, which is useful mainly for the &quot;Keys&quot; and &quot;Snippets&quot; pages that have long lists of items.</li>
</ul>
<p></p>
<h3 id="sdl3-goodies">SDL3 goodies</h3>
<p></p>
<p>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:</p>
<p></p>
<ul>
<li>The operating system&apos;s dark/light mode status can be checked. Lagrange has had bespoke code for this for macOS, iOS and Windows, but on other platforms, SDL3 is now queried for this information. The &quot;Use system theme&quot; toggle is visible in Preferences &gt; Appearance.</li>
<li>Native file open/save dialogs. There are now proper &quot;Open File...&quot; menu actions, and file pickers for importing user data, selecting a file to send in the upload dialog, and locating PEM files in the identity import dialog. &quot;Save As...&quot; actions were also added to save files to a specific location, taking advantage of SDL3&apos;s dialog API.</li>
</ul>
<p></p>
<h3 id="new-document-color-themes">New document color themes</h3>
<p></p>
<p>It has been a while since new document themes were added. There are three new ones in version 2:</p>
<p></p>
<ul>
<li>Arboreal: A green-hued variant of Oceanic, evoking the feel of a forest with various leafy and earthy greens, yellows, and browns.</li>
<li>Ember: Orange and yellow hues with gray, like the glowing coals of a dying fire. Naturally this works best in dark mode. In light mode, it is predominantly pastel orange shades. (Ember pairs great with the orange system accent color on the Mac, which I happen to use.)</li>
<li>Paper: An old paper book, with pale sepia tones.</li>
</ul>
<p></p>
<h3 id="terminal-ui">Terminal UI</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h2 id="the-reading-experience">The Reading Experience</h2>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="paginated-view-mode">Paginated view mode</h3>
<p></p>
<p>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.</p>
<p></p>
<img src="/gemlog/images/v2.0_pages.png" title="Solderpunk&apos;s &quot;Orphans of Netscape&quot; as a two-page spread (PNG, 158 KB)">
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="hyphenation">Hyphenation</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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).</p>
<p></p>
<h3 id="view-toolbar">View toolbar</h3>
<p></p>
<p>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.</p>
<p></p>
<img src="/gemlog/images/v2.0_viewtools.png" title="The view toolbar is visible in the top right corner, with auto-reload enabled (PNG, 59 KB)">
<p></p>
<p>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).</p>
<p></p>
<h3 id="books">Books</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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".</p>
<p></p>
<p>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.</p>
<p></p>
<p>Gempub has its own set of document theme and layout options, so you can customize the appearance of books separately from other pages.</p>
<p></p>
<h3 id="books-sidebar-and-the-bookshelf">Books sidebar and the bookshelf</h3>
<p></p>
<p>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.)</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="saving-pages-to-the-bookshelf">Saving pages to the bookshelf</h3>
<p></p>
<p>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.</p>
<p></p>
<h3 id="per-site-theme-settings">Per-site theme settings</h3>
<p></p>
<p>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.</p>
<p></p>
<p>These settings are part of the exported user data together with other site-specific settings.</p>
<p></p>
<img src="/gemlog/images/v2.0_site.png" title="Site-specific settings for cosmos.skyjake.fi (PNG, 52 KB)">
<p></p>
<h3 id="incremental-parsing-and-layout">Incremental parsing and layout</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h2 id="sidebars">Sidebars</h2>
<p></p>
<p>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.</p>
<p></p>
<h3 id="improved-lists">Improved lists</h3>
<p></p>
<p>The sidebars are all basically lists of various kind, so several improvements were made in the underlying ListWidget:</p>
<p></p>
<ul>
<li>Items can now have variable heights, so for example you can wrap a long heading to multiple lines in Outline.</li>
<li>Multiple items can be selected at the same time by holding the Shift key. This makes bookmark organization easier, for instance. Multi-selections have their own context menu that acts on all the selected items. The selected items can be dragged as a group to reorder them or move them into a folder.</li>
</ul>
<p></p>
<h3 id="feeds">Feeds</h3>
<p></p>
<p>Out of all the sidebar tabs, Feeds has been improved the most.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="reader-tab">Reader tab</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="tinylog-subscriptions">Tinylog subscriptions</h3>
<p></p>
<p>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.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">🌐</span><a href="gemini://tinylog.geminispace.org/skyjake">gemini://tinylog.geminispace.org/skyjake</a></div>
<div class="link gemini-link"><span class="link-icon">🌐</span><a href="gemini://station.martinrue.com/skyjake/tinylog">gemini://station.martinrue.com/skyjake/tinylog</a></div>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<img src="/gemlog/images/v2.0_tlg.png" title="Reader tab with a Tinylog entry that has a quoted response (PNG, 130 KB)">
<p></p>
<h3 id="subscribe-to-all">Subscribe to All</h3>
<p></p>
<p>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.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">🌐</span><a href="gemini://tla.stilvo.id/directory">gemini://tla.stilvo.id/directory</a></div>
<div class="link gemini-link"><span class="link-icon">🌐</span><a href="gemini://gemini.circumlunar.space/capcom/feeds.txt">gemini://gemini.circumlunar.space/capcom/feeds.txt</a></div>
<p></p>
<h3 id="bookmarks">Bookmarks</h3>
<p></p>
<p>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.</p>
<p></p>
<p>You can empty the trash via the Bookmarks menu or the sidebar context menu.</p>
<p></p>
<h3 id="outline">Outline</h3>
<p></p>
<p>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.</p>
<p></p>
<h3 id="keeping-things-in-order">Keeping things in order</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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).</p>
<p></p>
<p>(You can't drag the tabs between the left and right sidebars, though. The left/right assignment is still done in Preferences > Sidebars.)</p>
<p></p>
<h2 id="sync-and-internals">Sync and Internals</h2>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="syncing-up">Syncing up</h3>
<p></p>
<p>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).</p>
<p></p>
<p>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.</p>
<p></p>
<img src="/gemlog/images/v2.0_sync-prefs.png" title="Settings for the sync server are configured on the new &quot;Sync&quot; page (PNG, 49 KB)">
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="security">Security</h3>
<p></p>
<p>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?</p>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="frequency">Frequency</h3>
<p></p>
<p>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.</p>
<p></p>
<h3 id="internal-changes">Internal changes</h3>
<p></p>
<p>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.</p>
<p></p>
<h3 id="core-library-and-gmget">Core library and gmget</h3>
<p></p>
<p>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.</p>
<p></p>
<p>The library comes with a curl-like `gmget` utility for making network requests on the command line.</p>
<p></p>
<pre>
gmget: no URL given

Usage: ./gmget [OPTIONS] URL...

Options:
  -o FILE, --output=FILE  Write response body to FILE instead of stdout
  -I NAME, --ident=NAME   Use the Lagrange identity matching NAME
  -U DIR, --user=DIR      User data directory (default: platform-specific)
  -p, --print             Print response status and meta string to stderr
  -h, --help              Print this help and exit
</pre>
<p></p>
<p>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.</p>
<p></p>
<h3 id="client-certificates">Client certificates</h3>
<p></p>
<p>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.</p>
<p></p>
<h3 id="config-files">Config files</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<p>Other notable changes:</p>
<p></p>
<ul>
<li>One particularly big file has been the subscription cache that stores information about all the known feed entries. Version 2 stores this in a binary format for more efficient writing and reading (feeds.lgr).</li>
<li>Metadata about discovered system fonts is stored in a new system-fonts.lgr file, so the app does not have to enumerate the fonts on every launch. That could take a while if the system has hundreds of fonts installed.</li>
<li>Input field backups have been moved to a separate subdirectory (&quot;backup&quot;) so they don&apos;t litter the user directory.</li>
</ul>
<p></p>
<h3 id="mime-hooks-become-tools">MIME hooks become &quot;tools&quot;</h3>
<p></p>
<p>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.</p>
<p></p>
<p>There is no automatic migration for mimehooks.txt. You will need to reconfigure your content filters if you happen to use this feature.</p>
<p></p>
<p>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.</p>
<p></p>
<h2 id="summary">Summary</h2>
<p></p>
<p>Befitting a major version number bump, Lagrange 2 changes the core experience in several ways:</p>
<p></p>
<ul>
<li>Syncing means that all your devices have the same user data. No more missing identities or fragmentation of bookmarks, subscriptions, and reading history.</li>
<li>Reading feed entries (now also including Tinylog feeds) is a more streamlined experience thanks to the Reader tab.</li>
<li>Paginated view mode allows traditional book-style focused reading.</li>
<li>Enhanced Gempub support lets you enjoy Gempub books in true e-reader fashion.</li>
<li>The bookshelf allows saving long-form content, in addition to regular Gempub books, for reading later on any of your devices.</li>
<li>The per-protocol and per-site settings let you customize content to your liking.</li>
<li>The UI is nicer to look at and fits better into the operating system, thanks to system fonts, color Emoji, and rounded corners.</li>
</ul>
<p></p>
<h3 id="the-2-x-series">The 2.x series</h3>
<p></p>
<p>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.</p>
<p></p>
<p>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.</p>
<p></p>
<h3 id="thank-you">Thank you</h3>
<p></p>
<p>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.</p>
<p></p>
<p>Version 2.0 begins a new chapter for Lagrange. I hope you will enjoy using it as much as I do.</p>
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/lagrange.gmi">Lagrange</a></p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
<p></p>
    ]]></content>
</entry>
<entry>
    <title>Lagrange as a Web App: Running a Browser Inside a Browser</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/09/emscripten-lagrange.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/09/emscripten-lagrange.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-09_emscripten-lagrange.gmi</id>
    <published>2026-09-25T11:55:50Z</published>
    <updated>2026-09-25T22:14:33Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/lagrange.gmi">Lagrange</a></p>
<p></p>
<p class="lede">The other day, yesterday morning to be exact, I was thinking it would be pretty neat if Lagrange could be compiled with Emscripten so it could run locally as a web page.</p>
<p></p>
<p>I am no fan of bloated web browsers nor am I particularly fond of the web in general, but unfortunately it is THE mainstream computing platform nowadays. Any modern gadget that cannot access the web seems a bit pointless — detached from the real world, in a way. I have never felt a particular drive to learn web technologies beyond the basics of the classic LAMP stack and JavaScript. The myriad client-side frameworks frankly seem repulsive. There is so much constantly evolving complexity building on itself that it seems a waste of time to really get into it seriously. Knowledge and skills become outdated after a year or two. That being said, one can't deny that the web in 2026 is a capable and versatile platform for implementing many kinds of software. It is particularly useful when it comes to cross-platform apps, as demonstrated by how widespread Electron has become. A core tenet of the web is that it is a platform unto itself, providing the same environment regardless of the underlying hardware. This part I do appreciate even if in practice the implementation is far from ideal.</p>
<p></p>
<p>A big design goal of Lagrange is that it is cross-platform, able to run pretty much anywhere. So far this has meant macOS, Linux, Windows, Net/Free/OpenBSD, Haiku, iOS, Android, a handheld game console, and even the terminal through Curses. You may note that one huge platform has been missing from this list: the Web.</p>
<p></p>
<p>Would it even make any sense to run Lagrange as a web app? Technically speaking, not really. It would be slower, use more memory, and be contained into the browser's sandbox, unable to make direct network requests to Gemini servers. But web apps do have advantages, too: no need to install anything, no need to upgrade, and ubiquitous access from any (compatible) browser. In certain environments, let's say a company-issued laptop where you are bound by ZScaler and umpteen different software management policies — a random, purely hypothetical example — a web app may start to feel attractive.</p>
<p></p>
<p>Be that as it may, I still have no time or inclination to dive into web programming to port Lagrange to the web. </p>
<p></p>
<p>At least, not if I do it manually.</p>
<p></p>
<p>I was still curious enough about this, and since I do get a kick out of seeing Lagrange running on any and all platforms, I asked ✨AI✨ to do this for me: compile Lagrange using Emscripten and set up a secure WebSocket relay so the app can actually fetch content, like a real app would.</p>
<p></p>
<p>I have it available here:</p>
<p></p>
<div class="link web-link"><span class="link-icon">🌐</span><a href="https://lagrange.skyjake.fi/">https://lagrange.skyjake.fi/</a></div>
<p></p>
<p>It supports the desktop, tablet, and phone UI variants (autodetected), so you will get a different experience when using your desktop browser vs. mobile browser.</p>
<p></p>
<p>I'm actually quite amazed how well the app works as a WebAssembly executable. Big kudos to SDL for their Emscripten support! That is a crucial part of why this works in the first place.</p>
<p></p>
<p>This is an early build of Lagrange v2.0, meaning the changes so far are mostly under the surface: SDL3 migration, refactorings, and cleanup. Version 2.0 is mostly about this kind of restructuring, but I think a few user-facing features will appear at some point.</p>
<p></p>
<p>In most cases, using the native Lagrange is obviously the better choice, but if anyone new to Gemini wants to quickly see a demo of Geminispace, or show Gemini to someone (or maybe play a bit of SpellBinding during a meeting, hypothetically), it's difficult to beat the convenience of a visit to https://lagrange.skyjake.fi/. </p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Choosing Sides in the Holy War</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/09/choosing-sides-in-the-holy-war.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/09/choosing-sides-in-the-holy-war.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-09_choosing-sides-in-the-holy-war.gmi</id>
    <published>2026-09-06T11:55:50Z</published>
    <updated>2026-09-06T21:23:34Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/ai.gmi">AI</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/lagrange.gmi">Lagrange</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/programming.gmi">Programming</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/life.gmi">Life</a></p>
<p></p>
<p class="lede">Recently, I was asked if I would be willing to set a strict "no AI" policy on Lagrange. I will try to figure out my stance and justification for it in this post.</p>
<p></p>
<h2 id="disruption">Disruption</h2>
<p></p>
<p>The topic of Generative AI is something I've been mulling over for several months now. If you've done anything computer-adjacent over the past couple of years, you will not have missed (although you may have wanted to) the topic of LLMs and AI. The breathless hype; the aggressive adoption in the industry; the incredible and worsening crunch on memory components; the questionable billions of dollars being invested back and forth; the rush to build data centers; and, most pertaining to this post, the holy war in the open source community, declaring death to the clankers, may they pry my keyboard from my cold, dead hands. My own employer is very much on the AI bandwagon, and through this contractual obligation, I have had the opportunity to use frontier agentic models for software development. This is to say, I am not basing my opinions on hearsay or "vibes", but actual evidence and experience of using these tools.</p>
<p></p>
<p>As a flexible technology, Gen AI has many facets to it: speech, music, images, video, text, program code, to name a few. An interesting note to keep in mind is that all these have traditionally been something that only human skill and creativity could achieve at a high, meaningful quality. For example, text-to-speech was always robotic and stilted before machine learning entered the picture. With modern techniques, you can generate extremely natural-sounding speech. When it comes to the more creative arts, while technically proficient, the results of Gen AI lack soul. The same applies to prose as well as images and audio. You can generate paragraphs of pleasant, well-flowing text, but ultimately it is hollow and plastic-wrapped. So far, nothing has changed the fact that actual art is born when the human mind imbues meaning and message into a medium; it is fundamentally a form of communication out-of-band with its actual physical manifestation.</p>
<p></p>
<p>Gen AI has been progressing at a rapid rate. A few years ago we were shaking our heads and/or astonished by the first image generation models that managed to actually produce convincing results (extra fingers notwithstanding). The image generation then quickly ramped up in quality and speed, soon turning into video generation. Nvidia is about to grace us with DLSS 5, real-time neural augmentation of video games to make the visuals more "realistic". In practice, this is Gen AI transforming rendered graphics at a rate of tens of images per second, on your own GPU. The same kind of curve of rapid progress has been evident in the textual domain, with GPT models evolving from nonsensical paragraphs to highly plausible-sounding, glossy articles. Then we got interactive chatbots, and out of those, the coding agents. Today, those agents are able to traverse massive code bases and comprehend them to such an extent that they are able to discover novel exploits and bugs.</p>
<p></p>
<p>It is evident to me that we are once again living through a great disruption in the world of computers. The previous great shift was the rise of the smartphone, the little device that powers our daily lives nowadays in large parts of the world. That shift has impacted and continues to impact society as a whole. I believe Gen AI is also going to have lasting repercussions for years to come.</p>
<p></p>
<p>When it comes to open source projects in particular, we are also faced with the acerbic force of activism. People who work on open source tend to have certain philosophical and dogmatic convictions about freedom and liberties, and an inherent assumption of reciprocity: if you benefit from my lovingly crafted code, you better also contribute something back, so we can all enjoy the virtuous cycle of progress. This is a great model for something that, for a large part, does not rely on monetary incentives. However, now that multiple large corporations are hungrily using all that open source code (and rare books, all of the internet, and whatnot) to train their LLMs, open source folks are appalled. They feel ripped off. Here are the billionaires extracting even more (unearned!) wealth from the fruit of our labors. Add to that the high costs of producing these models: using up valuable resources like water and electricity, polluting the environment with toxins and noise, and drowning the internet in a raging torrent of scrapers that download each and every page they can find. The full picture is quite unpalatable.</p>
<p></p>
<p>Objections in the realm of software do not end at the ethics, though. When you start using modern coding agents to develop software, it involves a fundamental trade-off where you give up control and low-level understanding but get the ability to achieve more complex, wide-ranging objectives. But what about code quality? Can you even understand what it does? Valid concerns; there were similar ones when we started using compilers instead of writing assembly language by hand. Another point of comparison is the whole industrial revolution where highly-skilled artisans were being replaced by production lines and factories. I understand it involved quite a bit of unrest. (For instance, famously, a certain bunch of textile workers in England got really upset about being supplanted by machines and went on quite the rampage.) That is to say, we've been here before, and past experience shows people will not take it lying down when their livelihood and core identity is being threatened and rendered obsolete.</p>
<p></p>
<p>Therefore, it is easy to understand why many are upset and willing to take extreme positions to defend their view of internet decorum and how software should be written and distributed.</p>
<p></p>
<h2 id="pragmatism">Pragmatism</h2>
<p></p>
<p>Given all this messiness, what should my personal stance be?</p>
<p></p>
<p>As mentioned above, I have been using frontier models and coding agents for several months at work. This was when I discovered a key factor in my own stance: if I don't really care about the code personally, why should I spend precious time and energy writing it by hand? The company only cares that the objectives are met; the code is a mere implementation detail.</p>
<p></p>
<p>When it comes to hobby projects like Lagrange, I do very much care about the source code, how it is written, and which features are included and which are not. Nine years ago, I designed and wrote a whole library (the_Foundation) so I could make complex C programs in the specific way that I prefer: low-level and understandable from top to bottom. Lagrange is built on this library as well. So, I tend to be biased against introducing factors that are counter to these principles.</p>
<p></p>
<p>However, software projects are much more than source code. For instance, there is a whole lot of busywork when it comes to compiling portable code on multiple platforms: build environments, dependencies, release automation, packaging, and distribution. Necessary and useful things, but not a particularly fun way to spend one's free time. I have used coding agents to help with this kind of scaffolding. It has saved time and helped improve the project's (modest) infrastructure. As a concrete example, the apt.skyjake.fi Debian repository is totally vibecoded, managed by a systemd-run bot that builds packages in the background automatically as versions are published. I could've learned how to set up the Docker containers for the various versions of Debian, fixed the details of the cross-compiling, and set up scripts to compose the Apt repository, but I judged this was several weeks' worth of time better spent on actual improvements in the app. To put it bluntly: I didn't really care about these aspects, so outsourcing them was acceptable.</p>
<p></p>
<p>I am not an activist nor an idealist. I have no burning desire to join the holy war against AI. History shows that these disruptions are a wildfire that ravages the land until new life can grow again, and that fighting the blaze, while brave and commendable, is ultimately futile. Alas, the voices and actions of individuals are no match against the crushing pressure (the billions of dollars) of an industry seeking to increase its profits and productivity. Capitalism paired with the collective action problem is firmly driving this train. (See also: the lack of any meaningful response to climate change.)</p>
<p></p>
<p>I find my views on Gen AI aligning quite well with Linus Torvalds: now in the fall of 2026, it is a useful and capable tool, but you should still keep a tight hold on the reins and be mindful of what you're doing. Be the one with the vision, and make sure it is your vision and not the machine's.</p>
<p></p>
<h2 id="the-ai-policy-tl-dr">The AI policy (tl;dr)</h2>
<p></p>
<p>Summarizing this into an actual policy:</p>
<p></p>
<ul>
<li>Gen AI _prohibited_ for art and images (e.g., icons, illustrations), documentation, user-facing text like UI strings (including translations) and release notes, app design, and feature scoping. [These things require a human touch.]</li>
<li>Gen AI allowed for build automation, as a debugging aid, testing, quality control, research, and code drafting. [These may be prohibitively time-consuming for a hobby project.]</li>
<li>PRs and bug reports clearly written by AI will NOT be reviewed by a human, i.e., me. [Don&apos;t waste my time.]</li>
</ul>
<p></p>
<p>For a "no AI" purist, this probably sounds like the misguided ramblings of a slophead. You may be thinking that "wtf, Lagrange is now a 24/7, slop-around-the-clock, pile of AI garbage." Well, I have been writing Lagrange since 2020 (by hand, naturally) with the very pragmatic standpoint of making it the perfect Gemini client for my own needs. (After all, what is Gemini about if not empowerment to implement one's own clients and servers.) I don't intend for that to change any time soon, even if certain aspects will be handled by a coding agent. Gemini is still about keeping things as simple as they need to be; no avalanche of vibecoded features will be forthcoming.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Lagrange v1.21: Inline Prompts, Gopher-over-TLS, UI Improvements</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/08/lagrange-1.21.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/08/lagrange-1.21.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-08_lagrange-1.21.gmi</id>
    <published>2026-08-26T11:55:50Z</published>
    <updated>2026-09-30T22:17:31Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p class="lede">This release is largely about user interface improvements. The new default behavior for input prompts is to appear inline on the page, exactly like images have always done. I've also addressed a few rough edges here and there to make the UI sleeker.</p>
<p></p>
<div class="link web-link"><span class="link-icon">🌐</span><a href="https://codeberg.org/skyjake/lagrange/releases/tag/v1.21.2">Download Lagrange v1.21</a></div>
<p></p>
<h2 id="inline-input-prompts">Inline input prompts</h2>
<p></p>
<p>Ever since the first version of Lagrange, when the server has requested input from the client, a modal dialog has appeared where the user can enter text. Why a modal prompt? While this is a distinct back-and-forth between the client and server, it does not involve changes to page contents, so it exists kind of outside the normal model of browsing: the user has effectively left the page since they opened a link and are now at a URL that resulted in a query prompt, however they haven't received a new document so there is nothing else to switch to yet (except an empty page). The modality is a cheap and straightforward way to prevent the user from accidentally performing any actions on the (now obsolete) document being shown.</p>
<p></p>
<p>The idea of displaying prompts inline with page contents has been around for several years now.</p>
<div class="link web-link"><span class="link-icon">🌐</span><a href="https://github.com/skyjake/lagrange/issues/127">I think the earliest documented mention is from 2021.</a></div>
<p>By turning the prompt into an inline element, a sub-component of the current page, its place in the model is resolved neatly. Exactly like an inline image, it can now be treated like a part of the page when it comes to navigation and history.</p>
<p></p>
<p>The challenge with this is that it is quite an invasive change. Of course, the internal machinery for inline content exists (for both images and audio), but input prompts have some unique properties to consider. First of all, they are UI widgets, so they live on an entirely separate layer compared to page contents. Also, they require capturing keyboard input focus, backing up any text entered by the user, and if you navigate back and forward in history, the prompts on the page must be part of the cached state. After managing to iron out all these details, I realized there was also a unique problem when it comes to the navigation swipe animations used on mobile devices and macOS. Each prompt is a widget that needs to sync its position with the page contents. Easy enough: we have both the vertical scroll position and the horizontal swipe offset. But during a swipe, the tab is actually showing two overlapping document views, and prompt widgets must appear properly layered between them. This was not exactly trivial to get right. (This area of the code was not straightforward to begin with.) In the end, I had to resort to excluding prompt widgets from normal drawing and introducing a new manual child drawing step between the view layers.</p>
<p></p>
<p>A few releases ago I added the option to mark certain URLs as "assumed to require input", which means the app will not fetch those URLs without first asking for a query string from the user. With prompts now appearing inline, this affords them to be displayed immediately when the page loads. The end result is that it is finally possible to play prompt-loop games like SpellBinding in the ideal way: type a word, press Enter, type the next word, press Enter... Your hands don't need to leave the keyboard, no shortcuts need triggering, the prompt is always visible, and input focus stays inside the prompt.</p>
<p></p>
<p>Social capsules like Station and BBS also benefit from inline prompts: the post/reply/comment fields appears inline, and the rest of the page contents (comment thread, etc.) can be conveniently browsed and quoted. You can even navigate back in history to reference some other material and then return back to the still-open prompt.</p>
<p></p>
<p>All in all, I think inline prompts are the biggest step forward for the app's user experience in years.</p>
<p></p>
<img src="/gemlog/images/v1.21_prompt.png" title="Screenshot of an inline prompt (56 KB)">
<p></p>
<h2 id="unfocused-input-prompts">Unfocused input prompts</h2>
<p></p>
<p>The mobile UI — the phone variant in particular — does not have quite enough real estate to show fully-functional prompts together with the page contents. Therefore, I've also made the previously strictly-modal top/bottom-attached input prompts less strict. Clicking or tapping outside the modal prompt relinquished focus to the document, enabling the rest of the app UI as well, so you can access the page contents, sidebar, and other parts of the app normally, while keeping the prompt open but unfocused. However, changing the current page will dismiss the prompt and returning to it may not be possible.</p>
<p></p>
<p>This new behavior applies to top/bottom prompts on all platforms.</p>
<p></p>
<h2 id="gopher-over-tls">Gopher-over-TLS</h2>
<p></p>
<p>One notable new feature of v1.21 is the sometimes-requested "gophers://" support. There is not much to it, really: it is Gopher over a TLS connection. Gopher TLS certificates get the Gemini-style TOFU treatment, using exactly the same code that the Gemini side is using.</p>
<p></p>
<div class="link web-link"><span class="link-icon">🌐</span><a href="gophers://bitreich.org/">gophers://bitreich.org/</a></div>
<p></p>
<h2 id="tab-bar-layout">Tab bar layout</h2>
<p></p>
<p>The most prominent UI change is perhaps that the tab bar now appears at the same level as sidebar tab buttons. Previously, the document tab bar extended edge-to-edge across the entire window, and if the sidebars were open, the tab bar looked out of place compared to the document contents. Moving the tab bar into the document area also means the UI chrome at the top of the window can be slimmer. In other words, a more compact and better-aligned layout.</p>
<p></p>
<h2 id="persistent-window-arrangement">Persistent window arrangement</h2>
<p></p>
<p>While Lagrange has always saved the places of open windows when quitting, that placement has not been respected when opening new windows later on. The app has only remembered one default position and size that all new windows have used.</p>
<p></p>
<p>In v1.21, I've made window arrangement persistent: if you open three windows and arrange them in a particular way (e.g., side by side), you can close one or more of them, and when you open new windows, they will reuse the vacant slots in the previous arrangement. This is a nice little usability improvement if you frequently use multiple windows.</p>
<p></p>
<h2 id="better-middle-button-scrolling">Better middle-button scrolling</h2>
<p></p>
<p>Middle-button scrolling was added in the previous release. I'm a wheel-scroller myself, so I confess I didn't get this quite right. In v1.20, you needed to drag with the middle button to scroll, but if you actually do this for any extended period of time, it quickly starts tiring your finger. In v1.21, middle-button scrolling has been improved:</p>
<p></p>
<ul>
<li>A middle-button click at a non-interactive location toggles scrolling mode (cursor switches to an up-down arrow ↕). You can then scroll at your leisure without holding down a button.</li>
<li>A middle drag still works, and is useful if you need to scroll where there are only interactive elements, like list items or lots of links.</li>
<li>Scroll bars check for the middle button as well (previously only documents and lists responded to it).</li>
</ul>
<p></p>
<h2 id="windows-msvc-build">Windows MSVC build</h2>
<p></p>
<p>Work has been progressing toward making Lagrange compile using MSVC on Windows, using Microsoft's build tools. Thus far, building Lagrange has required a run-time or compile-time POSIX compatibility layer like MSYS2 and MinGW. Version 1.21 is still built with MinGW, but it is now also possible to use MSVC with Microsoft's vcpkg tool (it takes care of the dependencies like the_Foundation and Harfbuzz) to compile a fully-functional executable.</p>
<p></p>
<p>I haven't yet decided if/when the prebuilt Windows binaries will switch to this. One downside of vcpkg is that it is not a light-weight tool: all the dependencies are built from source, which makes the process take a looong while and use oodles of disk space. My Windows build machine is a VM with meager resources so vcpkg may not be the right fit.</p>
<p></p>
<p>Many thanks to @gim913 for their contribution to this new option.</p>
<p></p>
<h2 id="arm64-build">Arm64 build</h2>
<p></p>
<p>I have a Raspberry Pi 400 that will be serving as a build machine so now there are AppImages for 64-bit ARM systems as well.</p>
<p></p>
<h2 id="other-small-things">Other small things</h2>
<p></p>
<ul>
<li>On Windows and Linux, closing the last tab no longer quits the app but instead gives you a blank tab. (You&apos;ll need to explicitly trigger the Quit action to the quit the app.) This brings the behavior in line with macOS and the mobile versions.</li>
<li>Added an appearance setting for making scroll bars thicker. (For those who enjoy the old ways...)</li>
<li>Parsing ZIP archives is more robust. The format allows for arbitrary data to be prefixed to the file, like an executable stub for self-extracting archives, for instance. Lagrange can now detect and read this kind of archives, too.</li>
<li>The missing &quot;nex://&quot; and &quot;misfin://&quot; URI handlers are now registered, so the operating system knows to open Lagrange with these.</li>
<li>Debug Information (&quot;about:debug&quot;) lists which optional features are enabled in the build.</li>
<li>Fixed various small issues with Gopher, URI handling, text rendering, sidebar behavior, and TUI layout.</li>
</ul>
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/lagrange.gmi">Lagrange</a></p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Re: Dead ends</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/08/re-dead-ends.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/08/re-dead-ends.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-08_re-dead-ends.gmi</id>
    <published>2026-08-03T11:55:50Z</published>
    <updated>2026-08-03T20:00:53Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/programming.gmi">Programming</a></p>
<p></p>
<div class="link gemini-link"><span class="link-icon">🌐</span><a href="gemini://nthcdr.eu/blog/2026-08-03-dead-ends.gmi">gemini://nthcdr.eu/blog/2026-08-03-dead-ends.gmi</a></div>
<p></p>
<p>As someone who has a fair share of hobby projects waiting in the drawer, I can say this is an opportunity to consider your true motives for working on these "failed" efforts.</p>
<p></p>
<p>A hobby project is fueled by excitement and the enjoyment one gets for creating something new. The unfortunate truth is that excitement is always fleeting and, once a software project grows larger, it is less and less enjoyable to work on. The increasing cognitive load makes the process more and more challenging as time goes on. It is only natural that eventually greater enjoyment is found in something else.</p>
<p></p>
<p>Software projects are never finished, per se. One would have to be exceptionally rigorous to decide on set-in-stone end state criteria for a hobby project and then work toward them, and nothing else, until the objectives are met. That does not sound very fun at all, especially when excitement and enjoyment are the primary motivators.</p>
<p></p>
<p>That is not the core issue at hand, though. A more interesting question is, what makes you think a dead hobby project was a failure? It succeeded in delighting and holding your attention for a few days/weeks/months. The truth is, that was the project's primary purpose. No one (probably) asked you to create the project in the first place, so no one is let down that it didn't reach whatever end state you may have imagined it to have. This mismatch between one's unrealistic hopes and dreams and the reality of software development is ultimately the problem. The goal posts for a hobby project can be nebulous and ever-shifting, so it is more likely than not that you were never going to reach them anyway. In the end, one just need to look at this with the correct perspective: the glass is not half empty because the project was unfinished; the glass is half full because you created something out of nothing and it made you happy.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Chaos that Wears an Alluring Mask</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/07/chaos-that-wears-an-alluring-mask.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/07/chaos-that-wears-an-alluring-mask.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-07_chaos-that-wears-an-alluring-mask.gmi</id>
    <published>2026-07-19T11:55:50Z</published>
    <updated>2026-07-22T19:54:13Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/life.gmi">Life</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/ethiopia.gmi">Ethiopia</a></p>
<p></p>
<p class="lede">I've been exploring Addis Ababa for the past week and a half.</p>
<p></p>
<p>The capital of Ethiopia is a sprawling, busy city where modern high-end living can be found right next to rubble and poverty. The luxury Airbnb apartment where we are staying (a ridiculous 4-bedroom, 5-bathroom penthouse) is an example of wealthy upper class lifestyle. Looking out the window on the 6th floor, I can see a slum of slanted shacks built from metal sheets next to a field where lambs are grazing. The busy street below contributes a constant hum of cars and the occasional beep of a horn. All around the city, new buildings are being constructed. I am told the poor are being continually pushed out to clear way for new projects. There is no end in sight for the rapid growth and development of the city. It seems everywhere you look there are entrepreneurs, street vendors, or an opportunity waiting for the right person.</p>
<p></p>
<p>From a Nordic perspective, the slice of life I've witnessed has an improvised, organic quality. Functions often automated with machines in Europe, like ATM security and parking fees, are handled by actual people, sometimes just a person walking around the lot in a high-vis vest. Another example is the behavior of drivers on the streets: most cars are small and nimble, and the flow of traffic is akin to that of a mass of people walking on sidewalks, where cars intermingle haphazardly and, instead of relying on lights and signs, trust the cooperation of other drivers. It is not uncommon for one to U-turn directly into oncoming traffic, inching into the opposing lane until the cars decide to give way. Horns are commonly used to tell pedestrians that they've been seen and, no, we're not going to stop for you. People cross the streets and walk among the traffic with confidence, cars passing by only centimeters away. One time, I saw a person in a wheelchair proceeding along the middle divider of a busy street, apparently on purpose. Yet another example is grocery stores, where you find mismatched collections of items, and may receive a dozen eggs in a cardboard box originally intended for AA batteries. And speaking of stores, outside the city center you can see small stores everywhere: tiny rooms open to the street with collections of fruit, produce, flowers, car parts, clothing, and electronics. Often you see an entire stretch of street full of stores selling just wheel caps, ceiling lamps, jewelry, or a particular type of traditional white dress. There is structure to it, but it feels naturally grown, not architected and rigid like in Finland.</p>
<p></p>
<p>Mobile phones are everywhere, of course. Even in the suburbs, you can see people walking by or sitting outside, staring at their screens. (Typically black Android slabs.) It is perhaps the most jarring juxtaposition of modern and traditional life, but par for the course in 2026, I suppose. Cell tower coverage in the city is excellent, with 5G or 4G everywhere. Browsing Gemini is no different than back home, and hardly taxes my 1.5 GB roaming data plan. Latency to the home server in Finland is excellent despite being half a world away. We haven't ventured too far out of the city, except into the surrounding hills, but this level of cell coverage is obviously an urban privilege.</p>
<p></p>
<p>I've been very impressed by the multitude of parks inside the city. They are large and well-maintained, with many beautiful flowers and trees. However, one of the newest, Unity Park, struck me very much like an artificial tourist attraction in comparison to the others. Instead of focusing on pretty nature, Unity Park has a sizable children's playground, two zoos, and a set of modern artworks, with the actual historical buildings (the palace of King Menelik II and an archive related to the birth of the African Union) almost feeling like an awkward sideshow among the obviously arranged spots where tourists can take selfies. It is evidently working, though, because the place was filled with foreign visitors.</p>
<p></p>
<p>It is the rainy winter season now in July, so every day we've been having thunderstorms, rain showers, and cold evenings. The weather is eerily similar to Finland's summer, although ironically, it's been nearly 30 °C back home while here in Addis it's around ten degrees colder. The stark difference here is the sunlight. We are practically on the equator so the sun is directly overhead with an UV index of a crisp 10 or 11 on most days. The city is at the altitude of 2.2 km, and you can certainly feel it when climbing the steeper hills and longer stairs: immediately you find yourself breathing heavier in the thinner air. The highest peaks go above 3 km. In Entoto Park, north of the city, we got as high as 2.9 km, where they have paved walkways in the forest across the mountainside. It was a wonderful surprise encountering wild monkeys among the trees. The group, roughly six of them, eventually stopped leaping between branches and descended to the pathway, gazing warily at us. I suppose they are used to humans passing by.</p>
<p></p>
<p>There is a clear western and U.S.-style capitalist influence that permeates the city. In addition to the high rises, spa resorts, malls, and restaurants (including a KFC imitator called KKFC), take for example the former palace of Emperor Haile Selassie, deposed in 1974. The palace, its surrounding gardens, and the collection of treasures within — largely gifts from foreign rulers, like intricate ivory carvings and gold statuettes — was closed from the public until a couple of years ago, but now having been renovated, is a tourist trap for affluent visitors to explore. The commercialized nature of the site is highlighted by the presence of multiple restaurants and cafes on the premises, and there is even a fancy spa next to the main building inside the palace gardens. Overall, it is an impressive place for sure. Standing next to the personal car collection and white train cars of the former Emperor, peering into the kitchenette of the train where food was prepared for the esteemed passengers, you feel surrounded by the ghosts of the ruling class of Ethiopia.</p>
<p></p>
<p>The omnipresent coffee and the many cafes, seemingly a couple of them in every city block, are a personal highlight. I love coffee, and the strong, dark brew they make here is powerful and delicious. My caffeine intake must be off the charts but what can you do? (Have another Macchiato, perhaps.) When it comes to food, there is of course injera accompanied by various sauces and sweet fresh fruit. A ripe mango here is so much richer in flavor than the sad specimens available in Finland (shipped raw from overseas). One night, we went to a (touristy) restaurant where they play live music and perform traditional dances while you eat injera. It was memorable, though a tad loud. I must say that the tastiest injera and sauces were not available in the restaurants but at the home of the relatives we were visiting. I'm sure there are many small restaurants hidden among the unpaved side streets of the city, serving similarly excellent food, but finding such places is a challenge. Venturing off the busier streets, into muddier ones where stray dogs sniff around, water is running out of a broken pipe, and uneven stones may shift underfoot, requires a certain level of courage. The city is relatively safe — typical metropolitan pickpockets aside — but being the conspicuous fair-skinned foreigner does tend to induce some anxiety. If nothing else, it is a stark demonstration of what it feels like to belong in a minority.</p>
<p></p>
<p>What I've seen so far is really just the façade of the city — the manicured parks and posh restaurants, and the painted fences and billboards that hide the more unseemly areas, messy construction sites, and slums. Behind the glittering main streets and upscale clothing boutiques, the back streets are busy with normal life, dust and mud, people milling about. It strikes me that the entire city is also a façade. It is a polished, safe front for a country that’s otherwise full of wildlife, vast wilderness, smaller rural towns, unrest among the ethnic groups, and (rumor has it) forces gathering to oppose the current government.</p>
<p></p>
<p>All of this is vibrant, unruly chaos that wears an alluring mask.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="https://gmi.skyjake.fi/gemlog/2026/07/../..">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Re: AI-assisted development vs. feature bloat</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/07/re-ai-bloat.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/07/re-ai-bloat.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-07_re-ai-bloat.gmi</id>
    <published>2026-07-17T11:55:50Z</published>
    <updated>2026-07-17T10:15:17Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/programming.gmi">Programming</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/ai.gmi">AI</a></p>
<p></p>
<div class="link gemini-link"><span class="link-icon">🌐</span><a href="gemini://nader.pm/ai-assisted-development-vs-feature-bloat/">gemini://nader.pm/ai-assisted-development-vs-feature-bloat/</a></div>
<p></p>
<p>I think this hits upon a key consequence of coding with AI. Disregarding all the negative externalities, the hype bubble, drive toward wealth inequality, and social posturing related to GenAI/LLM usage, as a practical result, you are able to dramatically lower the initial cost of software implementation. In software development, this cost is a big gating factor in what gets done, forcing developers to be meticulous about choosing what features to work on. However, with AI taking care of much of the work, the software can grow quickly, limited only by the developer's guiding vision. The true problem becomes apparent at a later stage when the software needs maintenance to remain viable for the long run. Feature-laden bloatware is difficult to keep up and running, let alone glitch-free.</p>
<p></p>
<p>It has been said that with AI-assisted coding, the invaluable contribution of the human developer is taste: the human must decide on the proper feature set, keep tabs on code quality and overall architecture, and have a coherent vision of what purpose the software serves. Without taste, the produced software is unwieldy slop.</p>
<p></p>
<p>A responsible developer is able to use AI to find and fix bugs, and improve the quality of the software in ways that would be prohibitively expensive otherwise, given constraints on time and energy. The real question is whether these advantages are worth the total cost of the technology. I also wouldn't ignore the human temptation of letting the AI genie make one's wishes come true: all the nice-to-haves and long-term blockers can be (potentially) solved with a few well-crafted prompts. How long can anyone resist that on high-minded, ethical grounds?</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="https://gmi.skyjake.fi/gemlog/2026/07/../..">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Gopher Rabbit Hole</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/05/gopher-rabbit-hole.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/05/gopher-rabbit-hole.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-05_gopher-rabbit-hole.gmi</id>
    <published>2026-05-27T11:55:50Z</published>
    <updated>2026-05-27T19:00:15Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/lagrange.gmi">Lagrange</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/gopher.gmi">Gopher</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/gmcapsule.gmi">GmCapsule</a>, <a href="https://gmi.skyjake.fi/gemlog/topic/programming.gmi">Programming</a></p>
<p></p>
<p class="lede">The plan was to fix a crash in Lagrange caused by not properly validating the content of Gopher type 0 selectors. I figured I need to run a Gopher server to test this properly, but the available options didn't strike my fancy. How hard is it anyway to write a Gopher server? Not hard at all, it turns out. A few days later, I had a new server written in C, serving my Gemini capsule content via skyjake.fi port 70.</p>
<p></p>
<p>Then I had a thought: since my Gopher server is taking Gemtext files and rendering them as fancy plain text, maybe I could use a higher-level markup and use that to generate BOTH the .gmi and fancy .txt variants?</p>
<p></p>
<p>A few years ago (...in 2002...) I wrote a document processing macro language inspired by TeX. It has a bunch of user-definable output filters that determine how the output gets generated, so you can use it to produce plain text, RTF, HTML, UNIX manual pages, etc. This would be a perfect (??) solution for my gemlog content: write in one format, generate all sorts of presentation formats. It didn't take much effort to make an output filter for Gemtext.</p>
<p></p>
<p>Of course, GmCapsule also needed a little tweak to support configurable format converters, but that wasn't very difficult to implement.</p>
<p></p>
<p>So, now I had a way to present my gemlog posts in any format, as long as the original source uses my macro language.</p>
<p></p>
<p>Then, for some reason, I felt it was necessary to add PDF output to my macro language. How cool would it be to view my gemlog posts in PDF?! I admit, typesetting is a bit of a guilty pleasure for me and getting reacquainted with this old document processing tool was quite fun. It's nice to have Markdown and HTML, but actual paper-like rendering? That's an exciting and wholly different ball game.</p>
<p></p>
<p>Now, at this point in my tumble down the rabbit hole, I figured it was time to stop wasting my own time and I turned to Claude to implement this "important" new PDF feature for me.</p>
<p></p>
<p>A few days later, having come back to my senses, I remembered to actually test and fix the Lagrange crash that set off this little quest.</p>
<p></p>
<p>I'd like to write more about my new Gopher server as I think it's pretty neat, but in the meantime, I'll briefly say it has helped tidy up the Gopher support in Lagrange nicely. The next patch (1.20.6) will include a number of Gopher-related improvements.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Spring Cleaning, Part 2</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/05/spring-cleaning-part-2.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/05/spring-cleaning-part-2.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-05_spring-cleaning-part-2.gmi</id>
    <published>2026-05-12T11:55:50Z</published>
    <updated>2026-05-12T19:27:39Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/gemini.gmi">Gemini</a></p>
<p></p>
<p class="lede">Keeping with the theme of spring cleaning, I decided to spend some time improving the machinery that runs this gemlog.</p>
<p></p>
<p>The posts have been individual files in a flat /gemlog/ directory, but after six years things have gotten a tad cluttered. It made more sense to have year/month subdirectories that keep the files neatly organized. The topic indices were also moved to their own subdirectory.</p>
<p></p>
<p>I tried to be careful to not break any old URLs by applying redirections and content formatting rules that fix internal cross-references between posts. The end result should be that everything works as before, including any external links pointing to the gemlog, except now everything is also tidy.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/atom.xml">The Atom feed has also been enhanced.</a></div>
<p>It should now have accurate publication and update timestamps for each entry (previously time was fixed at 12:00), and the entries now contain a full HTML-rendered version of the post. For that I reused some code from the web mirror.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
<entry>
    <title>Spring Cleaning</title>
    <author><name>Jaakko Keränen</name></author>
    <link href="gemini://skyjake.fi/gemlog/2026/05/spring-cleaning.gmi" rel="alternate" type="text/gemini" />
    <link href="https://gmi.skyjake.fi/gemlog/2026/05/spring-cleaning.gmi" rel="alternate" type="text/html" />
    <id>gemini://skyjake.fi/gemlog/2026-05_spring-cleaning.gmi</id>
    <published>2026-05-01T12:00:00Z</published>
    <updated>2026-05-01T12:00:00Z</updated>
    <content type="html"><![CDATA[
<p></p>
<p><span class="tag-icon">🏷</span> <a href="https://gmi.skyjake.fi/gemlog/topic/doomsday.gmi">Doomsday</a></p>
<p></p>
<p class="lede">I've been clearing cobwebs off the Doomsday codebase recently.</p>
<p></p>
<p>The project has been on hiatus. I last worked on this code 5-8 years ago. It's amazing how much of it I've forgotten. Doomsday represents a solid 20 years of invested time, and it shows. There is so much stuff: an amalgamation of Doom/Heretic/Hexen, a complete GUI framework, a smaller widget framework for the game menus, a scripting language, a document processing tool, partially finished next-gen renderer, scriptable 3D model system, and heaps of UI for configuration and package management.</p>
<p></p>
<p>The actual games having been largely left in vanilla state highlights where my focus has been over the years. I was never a game modder, concentrating instead on the scaffolding.</p>
<p></p>
<p>In practice, I've been making progress toward the 3.0 goals: SDL 3 migration, Qt 6 migration (still in use peripherally), superficial UI facelifts, better CMake structure and dependency management, completely functional MSVC build on Windows (instead of relying of MSYS or MinGW), and cleaning up the macOS and Linux builds. It's mostly maintenance, but I've appreciated the walk down memory lane. It's nice to see the games working as before.</p>
<p></p>
<p>Question is: do I still have interest in pushing Doomsday forward? The next-gen rendering is an interesting challenge, but it's also a huge investment.</p>
<p></p>
<div class="link gemini-link"><span class="link-icon">➤</span><a href="/gemlog/">skyjake&apos;s Gemlog</a></div>
<p>CC-BY-SA 4.0</p>
    ]]></content>
</entry>
</feed>
