No description
  • Scala 77.6%
  • Nix 11%
  • Shell 8.4%
  • Just 3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Børge Lundsaunet 4b23aea4d9 The palette is a row of equals
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>
2026-08-12 23:52:30 +02:00
app The palette is a row of equals 2026-08-12 23:52:30 +02:00
docs README: octo on top 2026-08-12 17:03:00 +02:00
scripts The keyboard is a mode, and search is how you get back 2026-08-12 22:01:55 +02:00
test The palette belongs to writing 2026-08-12 23:12:29 +02:00
.envrc add envrc 2026-08-12 16:53:06 +02:00
.gitignore Consume the shared binky-ui module 2026-08-12 17:27:07 +02:00
flake.lock The palette is a row of equals 2026-08-12 23:52:30 +02:00
flake.nix Take binky-base from the forge, not from /home/blund 2026-08-12 22:23:39 +02:00
justfile An emulator scenario: notes that survive being killed 2026-08-12 19:57:48 +02:00
README.md README: point at the one design doc 2026-08-12 22:24:35 +02:00

binky the octopus

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, and compact ∘ 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