No description
  • Scala 85.2%
  • Shell 7.2%
  • Nix 5%
  • Just 1.4%
  • Python 0.6%
  • Other 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Børge Lundsaunet 47768e0f0a Emulator e2e: SMS leg through the emulated modem; fixed port
The suite now enables the bridge in settings, injects an SMS via the
emulator console (adb emu sms send), asserts server-side that it became
a marked Matrix message filed under the SMS space, and that the space
tab + contact room render in the app.

The emulator also gets an explicit port (5584): a sibling project's
emulator may run at the default 5554, and a fixed distinct serial means
the suites never adopt each other's device.

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

Co-Authored-By: eca-agent <git@eca.dev>
2026-08-12 20:37:49 +02:00
app SMS bridge edge: receiver, SmsManager modem, settings toggle 2026-08-12 20:37:38 +02:00
build-tools-scratch Add BarCheck: screenshot pixel-probe for e-ink UI debugging 2026-08-12 16:08:06 +02:00
docs SMS bridge core: the phone is the modem, the homeserver is the store 2026-08-12 20:37:26 +02:00
scripts Emulator e2e: SMS leg through the emulated modem; fixed port 2026-08-12 20:37:49 +02:00
test SMS bridge core: the phone is the modem, the homeserver is the store 2026-08-12 20:37:26 +02:00
.envrc add envrc 2026-08-12 16:53:40 +02:00
.gitignore ink-chat spike: Scala 3 Matrix client for Mudita Kompakt 2026-08-11 22:39:28 +02:00
flake.lock Relock binky-base: the shared harness 2026-08-12 19:57:58 +02:00
flake.nix Build on binky-base: the shared APK pipeline 2026-08-12 19:20:36 +02:00
justfile Extract core laws into a hermetic suite; e2e keeps only server legs 2026-08-12 17:18:26 +02:00
README.md Emulator e2e: SMS leg through the emulated modem; fixed port 2026-08-12 20:37:49 +02:00

binky the octopus

binky chat

A minimal black & white Matrix client for the Mudita Kompakt (e-ink, AOSP 12, de-googled). Written in Scala 3, built without Gradle. The package (and the repository) keep the historical ink.chat / ink-chat identifiers.

Design

  • UI: pure black on white, bordered boxes with softly rounded corners, sans-serif, zero animations. Built with plain Android Views (no AppCompat, no Compose).
  • Matrix: no heavy SDK. The Client-Server API is JSON over HTTP: password login, long-polling /sync, send m.room.message. No Google services on the Kompakt means no FCM — the sync loop is the design.
  • Scala: immutable core, effects pushed to the edge, and constructive data modelling ("parse, don't validate"): all Matrix identifiers are opaque types with smart constructors (Ids.scala); raw strings exist only at the I/O boundary.
  • E2EE: matrix-sdk-crypto (vodozemac inside) via its UniFFI Android bindings. The OlmMachine is transport-agnostic: we feed it each sync's crypto delta, execute its outgoing requests over our own HTTP layer, and encrypt/decrypt at the timeline boundary (crypto/CryptoEngine.scala). Native .so libs are packed for arm64-v8a (Kompakt) and x86_64 (emulator).
  • SMS: an optional bridge folds SMS into Matrix — the phone is the modem, the homeserver is the store: per-contact rooms under an "SMS" space, and a reply typed in any Matrix client goes out as a text (docs/sms-bridge.md).

Building

Everything is driven by the Nix flake, with just as the front door:

just apk        # build the APK (offloadable) and sign it
just ship       # ...and sideload it onto the phone
just apk-local  # build here instead, no store round-trip (quick iteration)
just            # list every recipe

Why two layers. nix run apps execute on your machine — they cannot be distributed. Only derivations can, so the APK is one: nix build .#apk compiles in the sandbox and happily lands on a remote builder (just apk-remote passes --max-jobs 0, which forbids local building, to prove it). Dependency resolution is the one step needing network, so it is a fixed-output derivation (.#deps) pinning exactly the coordinates in scripts/deps.txt.

That derivation stops at zipalign. Signing stays local: the debug key never enters the nix store and is never copied to the builder, and because it persists in debug.keystore (gitignored) the signature stays stable, so adb install -r keeps working across rebuilds. The pipeline runs both ways — BUILD_DIR, ARTIFACTS_DIR and SKIP_SIGN are the only differences — so there is one pipeline, not two that can drift. It lives in binky-base and is shared by every app in the suite; scripts/build-apk.sh here only names the app and points at binky-base (a sibling checkout, or the locked flake input via BASE_DIR).

The APK pipeline (Gradle-free) is:

aapt2 (resources, R.java) -> javac (R) -> scalac (against android.jar)
  -> d8 (dex + AAR classes) -> zip (dex + native libs) -> zipalign -> apksigner

Testing

Three tiers, from pure to physical:

just test      # laws of the pure core: fast, offline, no server
just test-e2e  # JVM: core + client edge vs a throwaway local Synapse
just test-emu  # emulator: full UI flow + E2EE
just check     # hermetic: core laws + APK build via `nix flake check`

The core laws (test/core) also run as the flake check core-laws, in a sandbox containing scalac, ujson and nothing else — so "the functional core is pure" is enforced by the build, not by convention. The e2e suites stay plain scripts (flake apps wrapping scripts/): they want a live Synapse on a real port, KVM, and a phone-shaped emulator — none of which belong in a hermetic sandbox, and unlike the APK there is nothing to gain from distributing them.

The emulator e2e boots a headless Android 12 emulator, seeds a Synapse with rooms/messages, drives the real UI (login, room list, chat, send), verifies messages server-side, sends into an encrypted room — asserting the decrypted echo renders in the UI and only ciphertext reaches the server — and injects an SMS through the emulated modem to watch it become a Matrix room. Screenshots land in build/shots/.

Layout

app/AndroidManifest.xml   manifest (minSdk 26, target 32)
app/res/                  chat-specific resources (notification icon, strings);
                          theme, colors and the octo come from binky-base
app/src/ink/chat/         UI shell (activity, views, sync loop, store)
app/src/ink/chat/matrix/  pure Matrix core (JVM-portable, no Android)
app/src/ink/chat/crypto/  CryptoEngine: OlmMachine effectful edge
test/core/                pure-core law suite (also the core-laws flake check)
test/e2e/                 JVM e2e driver
scripts/                  build + e2e orchestration (deps.txt pins maven coords)
justfile                  task runner: build, sign, sideload, test

Device notes

Mudita Kompakt runs MuditaOS K (AOSP 12, API 32), no Play Services. Install by sideloading: just install (or adb install -r build/ink-chat.apk).