- Scala 79.6%
- Shell 11.9%
- Nix 5.2%
- Just 2.8%
- Python 0.3%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| apps | ||
| base | ||
| spec | ||
| .envrc | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| README.md | ||
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.