No description
  • Scala 79.6%
  • Shell 11.9%
  • Nix 5.2%
  • Just 2.8%
  • Python 0.3%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Børge Lundsaunet e08ae3802c The bell over a locked phone, now with a test that would notice
The clock does not show its ring screen on the lock screen of the
Kompakt. This does not fix that. It does establish what is and is not
true, which the app previously had no way to say.

TheBell has promised ItShowsItselfOverALockedPhone since the spec was
written, and nothing checked it. The laws cannot: it is entirely a
matter of the manifest and the window manager. The emulator scenario
could not either, because it only ever drove an unlocked device. So the
scenario now sets a PIN, turns the screen off, and waits out a thirty
second timer — "0:30", since a bare number in that field means minutes
and thirty of those is a long time to hold an emulator open.

It passes. It also passed before any of the changes below, which is the
useful part: on a stock API 32 image the current code already wakes the
screen and puts the bell in front of a secure keyguard. Whatever is
wrong on the Kompakt is not the three things that looked wrong from
reading the source, and now there is a green test saying so rather than
a plausible story.

The changes are defects on their own terms, found while looking:

USE_FULL_SCREEN_INTENT was never declared. Since API 29 a full-screen
intent is only honoured for an app that asks for it here, so the app
had exactly one working route to the ring screen — starting the
activity from the service — where it believed it had two. It replaces
DISABLE_KEYGUARD, which no code has ever used.

The manifest said showOnLockScreen, which is the older attribute and
means "over the lock screen, and across all users' windows"; the one
that means what this activity wants is showWhenLocked. The window flags
were carrying it.

RingActivity asked with flags deprecated in API 27. It now uses
setShowWhenLocked and setTurnScreenOn where they exist and the flags
where they do not — minSdk is 26, so both branches are real.

And it no longer asks to dismiss the keyguard at all. The promise is
that the bell shows over a locked phone, not that a bell unlocks one; a
phone that let itself in every morning at 07:30 would be a worse phone.
The emulator agrees the difference is real — DismissingKeyguardActivity
went from naming RingActivity to null, while the keyguard stayed
showing and occluded by it.

Verified by building .#checks.x86_64-linux.clock-emu on the builder,
before and after.

🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-sonnet-4-5)

Co-Authored-By: eca-agent <git@eca.dev>
2026-08-14 21:16:17 +02:00
apps The bell over a locked phone, now with a test that would notice 2026-08-14 21:16:17 +02:00
base chat gets the emulator tier the other four have had all along 2026-08-14 18:56:41 +02:00
spec The specs get a checker, and the checker gets opinions 2026-08-14 20:50:55 +02:00
.envrc Six repos that shared everything but a repo 2026-08-14 18:00:08 +02:00
.gitignore Six repos that shared everything but a repo 2026-08-14 18:00:08 +02:00
flake.lock nixpkgs, 10 August to 13 August 2026-08-14 19:16:33 +02:00
flake.nix The specs get a checker, and the checker gets opinions 2026-08-14 20:50:55 +02:00
justfile Six flakes become one, and base stops being a dependency 2026-08-14 18:14:37 +02:00
README.md Say the thing the code has been doing all along 2026-08-14 20:52:32 +02:00

binky the octopus

binky

Simple, accessible utilities for e-ink Android devices, and the shared foundation they are built from. Developed on the Mudita Kompakt, which is the panel every decision here is argued against; nothing in the suite assumes that particular device.

Simple is one thing per app, and a screen you read rather than one you watch. Pure black on pure white, no animations, one still page replaced by another — which is what an e-ink panel is good at, and the only thing it is good at.

Accessible is the same work twice over, and this is the part worth being explicit about. A display with two colours cannot say anything in colour, so state is drawn as structure: a filled box, an edge bar, a line struck through. And what is drawn is announced — every control says what it is and what it is doing out loud, because on a screen this sparse the spoken description is often the only place a control's state exists in words at all.

Those two constraints turn out to be one constraint. Designing for a panel that has no colour to spend forces the same discipline as designing for someone who is not looking at it, so the suite can afford to take both seriously rather than trading one off against the other. The obligations that follow from it are in spec/base.allium, and the emulator scenarios check them the only way they can be checked: by reading the announcements back off a running device.

base/            the shared foundation: the design system (ink.ui), the
                 shared non-Android code (ink.data), the APK pipeline,
                 and the emulator harness
apps/chat/       SMS, as a peer-to-peer conversation
apps/clock/      time and alarms
apps/keyguard/   quick settings and notifications on the lock screen
apps/notes/      notes
apps/reader/     feeds
spec/            what the suite does, in Allium — behaviour at the
                 domain level, independent of how it is built

Each app is a self-contained build that compiles base/src and base/res into its own APK. base/ ships no artifact of its own — its build products are source and rules.

Why one repo

The suite was six repos, and the seams showed: a change to the design system meant a commit, a push, and a nix flake update in five places before anyone could see it. The apps share a build pipeline, a test harness, a look, and a release cadence, so they share a repo.

Working in it

One flake at the root, one lock. Each component keeps an outputs.nix and assembles its own outputs out of the shared pieces, so what is genuinely per-app stays in the app; the root only flattens them into one namespace. A component's default output is its own name and everything else is prefixed:

nix build .#keyguard              the APK
nix build .#keyguard-checks       its laws and its APK, in the sandbox
nix build .#suite                 every APK
nix build .#laws                  everything provable without kvm
nix develop .#chat-emu            chat's emulator shell

Each app keeps its justfile, so the per-app workflow is unchanged: cd apps/keyguard && just ship still builds, signs, sideloads and grants. just at the root is for the suite as a whole.

Where work runs

The laptop is the weakest machine here, so the rule is that anything which can be described rather than performed is a derivation — and therefore goes to the build server, and therefore caches. That includes the tests and the emulator, which are usually assumed to be too impure to describe and are not.

nix run scripts exist alongside them for the second-by-second loop, and for the three things that genuinely cannot move: your phone, your signing key, and a device you plugged in.

The principle, the ledger of where the suite actually stands against it, and the remote-builder handshake are in base/docs/building.md.

What it does, as opposed to how

spec/ holds the suite's behaviour in Allium: the domain-level account of what these apps do, with the Android, the nix and the Scala taken out. It is not documentation of the code — it is the thing the code is supposed to agree with, and it is deliberately written so that it could be true of a rewrite in another language.

spec/base.allium       the obligations every app compiles into itself:
                       Level's arithmetic, the codec's tolerance, the
                       rule that the cheapest redraw never happens, and
                       what a control owes someone not looking at it
spec/chat.allium       conversations, and SMS as one of them
spec/clock.allium      when a bell rings, and what survives a reboot
spec/keyguard.allium   the lock-screen panel
spec/notes.allium      the document algebra, and finding a note again
spec/reader.allium     subscribing, fetching, and turning pages

It maps onto the three test tiers rather than replacing them. The laws already prove most of what the invariants assert — 20 rows, one row per notification key, a blocked control that is a fixed point of every path to a system call — so the spec's job is to say why those are the properties worth proving, and to make the gaps visible. Each file ends with its open questions, which are the things the code currently decides by accident rather than on purpose.

Those questions are the useful output. Several of them are the same question asked in two apps and answered differently: a blocked control that names its missing permission in one place and says nothing in another, an app that opens the settings screen you need and an app that quietly rings late instead. None of that is visible from inside a single app, which is most of the argument for one repo.