overengineer.it.com

Markix

Around a year ago, I found myself in a rather interesting place. I didn't have 20 or so dollars to renew my Pinboard yearly subscription. Pinboard is the best bookmarking website I've used. However, I very rarely used its social features. Most of the time, it was just a storage of links for me, organized by tags. So that basic part didn't seem too hard to recreate in the age of The Emacsification of Software.

I've been using Newsboat as my RSS reader, so I figured that a TUI-based bookmarking app would be the best fit. I find stuff to bookmark in one terminal window, then bookmark it in another.

Since it was supposed to be just a list of links that could be tagged and opened in a browser, and I wanted to learn Zig, I decided to create everything from scratch. I started a new project and named it Markix. And down the rabbit hole I went.

I created the basic TUI rendering logic, inspired by an abandoned 2D TUI engine written in Rust (I couldn't find the project, but I'll try to find it and link it here). Soon after, I forgot about Markix.

I came back to it after several months and really liked what I had originally built, so I decided to keep Markix as a standalone layout engine that could be used by different renderers: terminal, web, maybe SDL — basically, a poor man's version of Clay.

Now, Markix is a layout engine. You describe a screen as a tree of boxes — rows, columns, text, links, lists — and it resolves that tree against a width. What comes out is a rectangle for every box, and what you do with those rectangles is somebody else's problem: there is a renderer that writes HTML, a renderer that fills a canvas, and a renderer that draws to a terminal.

And I've finally finished my bookmarking application, Readmarks, which has now become an RSS reader/bookmarker.

overengineer.it.com is built on Markix too. The generator reads Markdown, turns each block into a box, resolves the tree at a seventy-character measure, and writes HTML in which every element already carries its position. No layout runs in the browser, and the only script on a page is a theme switch.

The source is on Codeberg. It is early, the API moves, and the terminal side gets more of my attention than it probably deserves.

Why

I realize that there is absolutely no reason to do this. There is no advantage whatsoever to making this thing. There are already a lot of good TUI libraries that provide everything needed to build this kind of thing.

It's just that when I started, I was really interested in how TUIs work and wanted to implement my own very minimal version. Pretty soon, it spiraled into researching different existing libraries and the techniques they were using. Afterwards, I just wanted to see if I could make the abstract representation of layout I had also render HTML and be responsive there.

Being unemployed also played a huge role.

Also, it feels fun to get the same UI in a TUI and on the web with just one line changed:

try render(&storage, site, route, .{ .web      = .{ .writer   = &writer } });
try render(&storage, site, route, .{ .terminal = .{ .viewport = &viewport } });

(not counting ~30 lines of separate entry points for TUI and web)

markix demo

What's next

I would really like to experiment with implementing a DSL for Markix. I want to adopt signals for state management and provide an out-of-the-box solution for it. So you could have a static markup language for designing the UI, a simple way to manage state, and possibly a drag-and-drop UI creation kit where you could design a TUI/web app with predefined widgets.

Since this is very much a hobby project and isn't meant to be used by anybody except me, I'm not sure when, or even if, any of this will be implemented.

Behold: The magnificent wave

Everything above, running. The color of a column is a function of the column rather than of the moment, so the wave moves through the rainbow instead of dragging it along — which also gives the diff something to skip.

The readout is the patch, counted: frames a second, damaged regions, boxes redrawn out of boxes in the frame, and bytes crossing from WASM to JavaScript.

Every column of that stroke is a box in a markix tree: an empty one the height of the offset, and under it a filled one stretched to meet its neighbour, which is what makes a list of heights a line. The tree is resolved once a frame, painted into a list of rectangles, and diffed against the frame before it, so what reaches the canvas is the difference rather than the picture. The readout is that difference, counted.