- Scala 85.2%
- Shell 7.2%
- Nix 5%
- Just 1.4%
- Python 0.6%
- Other 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| app | ||
| build-tools-scratch | ||
| docs | ||
| scripts | ||
| test | ||
| .envrc | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| README.md | ||
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.solibs 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).