- Scala 77.6%
- Nix 11%
- Shell 8.4%
- Just 3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Three controls of three different widths read as three different kinds of thing, and the switch was the worst of them: 67px as "edit", 76px as "done", so it grew under the thumb at the exact moment you had just pressed it. They are all one width now — "edit" and "done" land on the same rectangle, [220,308], and the only thing that changes when the mode changes is the word. Measured on the emulator: + todo [12..100], + voice [108..196], the switch [220..308], all 88 wide and 42 tall, with 24px of clearance between the group and the switch. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-fable-5) Co-Authored-By: eca-agent <git@eca.dev> |
||
| app | ||
| docs | ||
| scripts | ||
| test | ||
| .envrc | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| README.md | ||
binky notes
A minimal black & white notes app for the Mudita Kompakt (e-ink, AOSP 12). Companion app to ink-chat — same octo, same visual language, same toolchain.
The model
A note is a single document of blocks, mixed freely and in order:
- text — free-form paragraphs
- todo — checkable items; tick them straight from the note list (tap the square), strikethrough for done
- voice — clips recorded inline in the editor, playable from both the list and the editor
So one note can be "packing list" with an intro paragraph, ten checkboxes, and a voice memo at the end.
Design
Follows the binky design language (see binky-base/docs/design.md, the suite's only design doc — how it looks and how it's built):
- Two colors only: pure black on pure white (e-ink first)
- Rounded white boxes with 2dp black borders as the universal container
- Solid black boxes for buttons and checked state
- No animations, no ripples, no transitions
- Plain Android Views built programmatically in Scala 3 — no Compose, no AppCompat, no Fragments, no XML layouts
- The binky octo as launcher icon, splash screen, and quietly holding the bottom of the note list
- Words rather than icons:
+ note,+ todo,edit/done. Putting the keyboard away ends the edit, and the palette says so.
The document algebra
The editor is built on a pure, Android-free core (Doc.scala). Text
bodies form a monoid under glue (newline-join with blank text as the
absorbed identity), and a document has two canonical forms:
compact— the storage form: blank lines dropped, adjacent texts glued. A monoid homomorphism under document concatenation.normalize— the editing form: adjacent texts glued, plus a blank text block padding every voice clip that lacks an editable successor, so there is always a writing surface. Idempotent, andcompact ∘ normalize == compact.
Every gesture (promote line to todo, demote todo, insert voice, remove
block) is a pure Doc => Option[(Doc, Cursor)] that preserves the
editing form; the UI just renders the result and places the caret. The
laws are checked exhaustively over small documents:
just test # ~46k property checks on plain JVM
Laws stop at the process boundary, and that is exactly where this app
once lost notes: saving a second note loaded the first one back, threw,
silently got an empty list, and persisted that. So there is a second
tier — test/emu/scenario.sh, run on binky-base's harness with
just test-emu — which writes notes on an emulator, kills the app, and
insists they are still there.
Storage
Notes are persisted locally as a JSON blob in SharedPreferences;
voice clips live under the app's private files/voice/ directory.
All persistence flows through Store.scala — the single seam to swap
out when notes eventually sync to a server (notes carry stable UUIDs
and createdAt timestamps for that purpose). The manifest deliberately
requests no INTERNET permission yet.
Building
just ship # nix build .#apk (remote-buildable) + sign locally + sideload
just apk # build + sign only
just apk-remote # force the remote builder (fails rather than build locally)
just apk-local # everything on this machine, no store involved
just check # Doc algebra laws as a hermetic flake check
The pipeline is Gradle-free: aapt2 → javac (R) → scalac → d8 → zip → zipalign → apksigner. Only runtime dependency is ujson, pinned in
scripts/deps.txt — fetched once as a fixed-output derivation, so the
APK itself builds fully sandboxed (and therefore on a remote builder).
Signing stays on the dev machine: the debug key never enters the nix
store.
The pipeline lives in binky-base — it is the same for
every app in the suite, so scripts/build-apk.sh here is a five-line
wrapper naming the app. binky-base is a source dependency: checkout
builds compile a sibling ../binky-base (override with BASE_DIR),
while the hermetic build takes it as a locked flake input.
Layout
app/
AndroidManifest.xml
res/ app-specific resources (just strings); theme, colors,
boxes and the octo come from the shared binky-base module
src/ink/notes/
MainActivity.scala single-activity UI: note list + block-document editor
Model.scala Block/Note data (pure, no Android)
Doc.scala document algebra: canonical forms + editing ops (pure)
Codec.scala the JSON shape of a note (pure; primitives from ink.data)
Store.scala JSON persistence
test/
DocTest.scala exhaustive law checks for the Doc algebra
scripts/
build-apk.sh thin wrapper; the pipeline itself lives in binky-base
deps.txt pinned Maven coordinates (single source of truth)
test-doc.sh run the JVM law checks from the checkout
justfile the impure bits: signing, sideloading
flake.nix packages (deps, apk), checks (doc-laws), apps, dev shell