Pinephone first steps

After anki trashed my history, uploaded the trashed history to the sync server, and then repeatedly re-synced the trashed history every time I tried to restore from backup, I wrote my own spaced repetition app. It reads input from a markdown file, asks questions in the terminal and stores state in a json file. I wrote those few hundred lines of code in an afternoon and I've been using it ever since.

I wanted to run it on my android phone too, but after several days of struggling and several gigabytes of IDE downloads I lost interest.

This is my typical experience. Extending and customizing my laptop is a routine affair. Extending and customizing my phone is so painful that I give up every time. The software on my laptop feels like a comfortable old boot, worn in over years of tweaking and scripting. The software on my phone feels like a slot machine that moves all the buttons around once a month and holds my eyes open while the ads play. It's shiny and polished, but it isn't on my side.

The pinephone is a $150 mobile phone that aims to be able to run mainline linux. I bought one hoping to create an experience more like the experience of using my laptop.

I just finished the first milestone - porting my little spaced repetition app.

Installing an operating system

I installed mobile-nixos. The docs at the time were very barebones but samueldr was kind enough to walk me through it over irc and I wrote down the process here.

I haven't gotten around to writing a system configuration yet, but once I do it will be a single-click deploy with nixops deploy -d focus - a big improvement over the LineageOS update process. The default configuration looks like this.

Cross-compiling the dependencies

Since the phone is running a normal linux userland it is possible to compile directly on the phone. But my laptop compiles things ~10x faster so it seemed worth investing in cross-compilation.

The first step is getting hold of arm64 versions of all my dependencies. This is done in shell.nix.

I pin the version of nixpkgs to make sure that both local and remote development use the exact same version of each dependency.

Nix recently gained support for cross-compilation but many packages don't cross-compile successfully yet. So the trick is to setup for cross-compilation, but grab some packages from the native arm64 repo. These won't build from source on a non-arm64 machine but there are pre-built version available in the nixpkgs binary cache.

Zig also didn't set paths correctly in the resulting binary but this is reasonable - there is no way for it to know that I'm cross-compiling to an identical nix system so it defaults to some reasonable heuristics. I just patch the binary after compilation.

The build process is then either zig build local or zig build cross && ./sync.

Since both laptop and phone are running the same operating system with the same pinned dependencies I don't bother to use a virtual machine for local development. I can just mock out the device sensors in code and everything else will run the same.

Writing a simple GUI library

I'm writing an immediate-mode GUI library because I find them much simpler than most retained systems, in terms of both lines of code and mental overhead. Our Machinery recently gave a GDC talk that lays out the rationale.

Battery usage is often given as a concern, but on my laptop this app hovers around 1% cpu at all times whereas gnome-calculator reaches 20-30% whenever I press buttons. If this becomes a problem with more complex UIs I can add some caching at the draw command layer.

There are definitely some things that may be difficult to do well in immediate-mode, like complex reactive layouts, but luckily they aren't things that I care about doing on my phone.

I copied my renderer and font atlas from microui and I've built just enough UI library to get things working. Here is the entire UI for the spaced repetition app:

I really like that it's just straight code - not split across multiple files or classes or callbacks. You can just read it from top to bottom. If I want to abstract over some component or layout I can just put it in a function.

Obviously it needs a lot of work to improve anti-aliasing, fonts, layout etc, but I think those can be done without complicating the interface above.