- Scala 55.7%
- Shell 24.3%
- Nix 17.4%
- Python 1.9%
- Just 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
ACTION_UP carries a position, and it is not always the position of the last ACTION_MOVE. Ignoring it threw away the final segment of every gesture, so a drag stopped fractionally short of where the finger left the glass. With a hand this is close to invisible: a finger usually rests for a moment before lifting, so the last MOVE and the UP are the same point and nothing is lost. With injected events it is neither invisible nor intermittent in the way it first appeared — `input swipe` interpolates its MOVEs across the duration and then reports the endpoint in the UP, so a full-height sweep always arrived a few pixels short. Enough to miss the end of the bar, which is exactly the case a test is checking, and enough to be blamed on the harness. It was ours. UP now accumulates exactly as MOVE does. The value ends where the finger ended, which is what it always claimed to do. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-fable-5) Co-Authored-By: eca-agent <git@eca.dev> |
||
| docs | ||
| harness | ||
| nix | ||
| res | ||
| scripts | ||
| src/ink | ||
| test | ||
| .envrc | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| README.md | ||
| REVIEW.md | ||
binky base
The shared foundation of the binky suite (chat, notes, reader, …): the design system every app is built from, and the test harness every app is checked with. Pure black on pure white, no animations — built for the Mudita Kompakt's e-ink panel.
It started as binky-ui, and outgrew the name the moment it acquired
things that are not UI.
What's in it
src/ink/ui/Ui.scala the component factory: boxes (programmatic
drawables), labels, buttons, inputs, layout
src/ink/ui/Render.scala keyed diffing + burst coalescing: the e-ink
rule that the cheapest redraw never happens
src/ink/data/Json.scala tolerant JSON primitives: decoding is total,
and longs are written as numbers
res/values/colors.xml ink_black, ink_white
res/values/themes.xml Theme.Ink (incl. splash, animations off)
res/drawable/octo_splash.xml
res/drawable-nodpi/octo.png
scripts/build-apk.sh the APK pipeline, shared by every app
docs/design.md the rules: how it looks, how it's built —
the suite's only design doc
test/JsonTest.scala laws for ink.data
harness/ emulator e2e: boot an AVD, drive the real UI
src/ and res/ are compiled into each app. scripts/ and harness/
never ship — they build and drive the APK from outside.
ink.ui is Android code; ink.data must never import Android, because
it is compiled both into the APK and into plain-JVM law checks. Anything
shared that is not UI gets its own package rather than moving in with
ink.ui.
The consumer contract
Code and resources
The pipeline that does this — aapt2 → javac → scalac → d8 → zipalign →
apksigner — lives here, in scripts/build-apk.sh. It was identical in
all three apps, so each app now keeps only a thin scripts/build-apk.sh
saying who it is:
export APP_ROOT="$ROOT" APP_NAME="binky-notes" BASE_DIR
exec bash "$BASE_DIR/scripts/build-apk.sh" "$@"
It expects APP_ROOT/app/{src,res,AndroidManifest.xml} and
APP_ROOT/scripts/deps.txt, and consumes the module in two places:
- scalac —
$BASE_DIR/src/**/*.scalajoins the source list (app code doesimport ink.ui.{Render, Ui},import ink.data.Json). - aapt2 —
$BASE_DIR/resis compiled alongside the app's ownres/and both are linked; shared resources merge into the app's package, so manifests and themes can reference@drawable/octo,@style/Theme.Inkdirectly. Apps must not redeclare a resource name the module already provides — aapt2 will refuse the duplicate.
Because src/ is compiled into every APK, every consumer's
scripts/deps.txt must contain at least the coordinates in this repo's
scripts/deps.txt (today: scala3-library and ujson).
BASE_DIR resolution mirrors the hermetic/impure split of the build:
- checkout builds default to a sibling checkout,
../binky-base— edit here, rebuild there, no commit dance; - hermetic builds get the module as a locked flake input:
inputs.binky-base = {
url = "git+file:///home/blund/prosjekt/binky-base"; # or the forge URL
flake = false;
};
# … in packages.apk's buildPhase:
export BASE_DIR=${binky-base}
After changing binky-base, consumers pick it up with
nix flake update binky-base (checkout builds see it immediately).
The emulator harness
harness/run.sh boots a headless AVD, installs an APK, and runs an
app-supplied scenario against it:
bash "$BASE_DIR/harness/run.sh" \
--apk build/binky-notes.apk \
--app ink.notes \
--scenario test/emu/scenario.sh
(--activity defaults to .MainActivity; --keep leaves the emulator
running for the next run, --no-clear keeps the app's existing data.)
Each app wraps this in scripts/test-emu.sh, nix run .#test-emu and
just test-emu.
A scenario is a bash file with the helpers already in scope:
expect_text "no notes yet" # retries until it appears (or fails with a screenshot)
refute_text "no notes yet" # assert absent — settles, then insists
tap_text "+ note" # tap the view whose text contains this
tap_edit 0 # tap the nth EditText, top to bottom
type_text "milk and bread" # spaces and punctuation handled
hide_keyboard; back # keyevents, named
restart_app # force-stop + relaunch: the process boundary
serve_dir test/fixtures # exports FIXTURE_URL=http://10.0.2.2:PORT
step "what happens next" # a heading in the log
shot "after-restart" # a numbered screenshot
Assertions retry, actions don't: a UI that hasn't settled yet is normal,
but a tap that misses is a bug in the scenario. Every failure leaves a
screenshot and the last view dump in build/emu/. Views are matched by
parsing the uiautomator XML rather than grepping it, so & and " in
the text on screen match the text you wrote.
restart_app is the point of the whole thing. Pure laws cover pure
code; the harness covers what only breaks when state crosses a process
boundary — persistence, migrations, reinstalls. Both of the codec bugs
that cost this suite a day would have died on the first restart_app.
If ANDROID_SERIAL names an already-running device or emulator, the
harness uses it and skips the AVD boot — which is the fast path while
writing a scenario. It refuses to touch a device it wasn't pointed at.
Checks
nix flake check does two things:
compile— buildssrc/againstandroid.jar. The module's only build artifact is source, so "it compiles" is half its contract.json-laws— runstest/JsonTest.scalaon a plain JVM (just testlocally). Shared code gets shared tests:ink.data.Jsonexists because the same JSON bug ate data in two apps at once, and it lived in the one file no app's tests could reach.
The harness is exercised by the apps that use it.