When I set out working on Lagrange v1.4, the general idea was to break out the app resources (fonts, mainly) into a separate automatically downloaded component. With this, the app installer/package would become significantly smaller. I would then be able to offer a wider selection of fonts in the app.
Such downloadable resources need some sort of compression, and zlib is already part of the build, so basic ZIP archives were the obvious choice. I took some of my old C++ ZIP parsing code and converted it to C for inclusion into the_Foundation. It was quick and easy work.
I started thinking about downloadable resources in more detail. Turns out it has a surprising amount of complexity to it. There has to be metadata for versioning, hashes for validation, and cryptographic signing for security. The resources need to be cached locally whenever possible, but purgeable and downloaded again when needed. The app needs to have fallbacks when a resource happens to be unavailable. It makes sense to have multiple separate packages for related resources, but not too many since some resources only make sense if you have the full set (e.g., font styles and weights). The files have to be hosted somewhere and preferably mirrored elsewhere for more reliable access.
Faced with all this I decided to postpone the feature a little. There is certainly value there, but the main advantage gained from all this complexity is smaller download sizes? I suppose the need for it isn't so pressing at the moment, given all the other areas that could be improved in the app.
However, ZIP files are quite useful on their own. They are basically just a directory tree with files. Lagrange already supports the "file" scheme for accessing local files, so extending that to treat ZIP archives as directories is actually pretty trivial. It wasn't long before I had this up and running in the app.
While navigating in the ZIP internal directory structure, it became clear that any other local directory could be presented in the same way. So I added that, too, for the "file" scheme URLs. Now one can actually view the contents of the Downloads directory from within the app. Pretty revolutionary!
I'm a big fan of e-books, not having bothered with physical books ever since the first iPad was released. (Reading without a backlight? No, thank you.) A simple Gemtext based format feels very neat, and now with ZIP archives already implemented, everything was basically in place for making Lagrange access .gpubs. The only missing part was proper rendering of the book cover page. The MIME hook mechanism that that converts Atom XML to Gemtext is a perfect fit for this, and now it also generates a Gemtext cover page based on the .gpub metadata.
Gempubs have an index page and chapters, so wouldn't it be nice to view the index on the side while a chapter is open? One option would be to show the index in the sidebar, but there you can only have simple lists and no free-form Gemtext. It's good for outlines but not the index page.
The app is only able to display a single document at a time, which has been an issue since the beginning. In theory, one can run multiple instances of the process, but that has problems with (not) syncing data files and IPC is a tricky topic of its own.
This was an opportunity to tackle both issues at once, but how to display multiple documents at the same time? There are already multiple tabs, but much of the UI state is global, for example the color palette of the currently viewed page.
I decided to go with a solution that will benefit supporting multiple windows in the future. The UI is essentially a tree of widgets, so there is no reason there can't be two of them side by side. Many of the internal UI events anyway are already specific to certain widgets or identified widget subtrees. In a multiwindow setup, each of these widget trees would be housed in a separate window.
Actually having a second tree of UI widgets required making some non-trivial changes in the framework, but nothing extraordinary. Much of the global state, such the aforementioned color palettes, had to be duplicated to be specific to one of the trees. Most UI events can stay local to their own tree, so both sides can live their life not caring that the other exists. App-wide notifications can be handled by dispatching them to each root one by one. While implementing this I found a set of new bugs in my widget layout system — a common occurrence — and fixing those took a little bit of extra time.
A complete Gempub reading mode requires more than split view modes, though, and that will be a topic for the future. For the time being, view splitting on its own changes the app for the better in a fundamental way, making the browsing experience more versatile and powerful.
This way of working is not uncommon for me. In a project where the main motivation is having fun, it is natural to follow the unexpected trails that open up. While it is great to have a to-do list for longer term objectives and simply to not forget about all the small things, it can feel like a straitjacket if followed too strictly. A bit of serendipity fills the work with thrilling little surprises.
The original Gemtext version of this page can be accessed with a Gemini client: gemini://skyjake.fi/gemlog/2021-05_serendipity.gmi