Twig

Browser Android emulator

Android inside Docker — redroid on Apple Silicon (Colima), or budtmo/noVNC on Linux+KVM. adb from the host; companion over reverse.

Twig is Android-only. This repo runs Android inside Docker so you (or a coding agent) can install, debug, and pair the companion without a physical phone.

Browser UI  ──►  Docker Android  ◄──  host adb (:5555)
                     │
                     └── adb reverse :8787 ──► twig companion

Two profiles

ProfileWhenContainerBrowser UI
mac (default on Darwin)Apple Silicon / no KVMAndroid 16 redroid (twig/redroid:16…-patched) via Colimaws-scrcpy on :8000
linuxLinux host with /dev/kvmbudtmo/docker-android:emulator_14.0noVNC on :6080
bun run emu:doctor
bun run emu:prepare          # Colima + binderfs on Mac; KVM check on Linux
bun run emu:up
bun run emu:wait && bun run emu:adb
bun run emu:open

Force a profile: TWIG_EMU_PROFILE=mac|linux.

Apple Silicon (this is the Mac path)

  1. Colima provides a Linux VM; Docker runs there.
  2. emu:prepare loads binder_linux and mounts /dev/binderfs (required by redroid).
  3. Redroid boots Android 16 (arm64-only). emu:prepare builds a small libcutils patch so memfd works on Colima kernels that lack /dev/ashmem (stock redroid 16 otherwise crash-loops — see redroid-doc#869).
  4. haris132/ws-scrcpy mirrors the screen in the browser at http://localhost:8000 (best-effort; prefer desktop scrcpy if the browser UI flakes).
brew install colima docker docker-compose
brew install --cask android-platform-tools
colima start --cpu 4 --memory 8 --disk 60 --vm-type vz
bun run emu:prepare
bun run emu:up
bun run emu:open    # http://localhost:8000

Desktop fallback: brew install scrcpy && scrcpy -s localhost:5555.

Linux + KVM (noVNC)

sudo apt install qemu-kvm adb && kvm-ok
TWIG_EMU_PROFILE=linux bun run emu:up
bun run emu:open    # http://localhost:6080/?autoconnect=true

Install Twig onto the container

Expo Go will not work (sqlite-vec needs a real build). Wait until sys.boot_completed=1 and service check package reports found (right after emu:up, package/activity may be missing for a minute while zygote starts).

cd apps/native
bunx expo prebuild --platform android
# Expo's device picker breaks on redroid's emulator-5554 alias — build then adb install:
export JAVA_HOME="$(brew --prefix openjdk@17)"   # JDK 26 breaks AGP
cd android && ./gradlew :app:assembleDebug
adb -s localhost:5555 install -r app/build/outputs/apk/debug/app-debug.apk
adb -s localhost:5555 shell am start -n org.bhalala.twig/.MainActivity
bunx expo start --dev-client   # Metro for JS

Or: bun run emu:install path/to/app-debug.apk

Bun + native build: root bunfig.toml sets linker = "hoisted" so CMake does not see node_modules/.bun/pkg@ver/ paths. If apps/native/node_modules still has stale .bun symlinks, wipe nested modules and reinstall:

rm -rf apps/*/node_modules packages/*/node_modules && bun install

adb reverse works over TCP the same as a cable:

bun run emu:wait && bun run emu:adb
bun run companion
# In the Android UI: Twig → Laptop → enter the six-digit code

Ports

PortWhat
5555ADB
8000Browser UI (mac / ws-scrcpy)
6080Browser UI (linux / noVNC)
9000Container logs (linux profile)
8787Twig companion WebSocket (host), reversed into Android

If the browser screen is blank

Usually the Android container never finished booting, so there is no display compositor for ws-scrcpy to mirror.

Symptoms:

  • adb shell wm size → Can't find service: window
  • getprop sys.boot_completed empty
  • Browser UI loads, but stream fails / stays black

Fix:

# Use Android 14 (16 often stalls under Colima)
bun run emu:down
# wipe half-booted data
docker volume rm twig-android_twig-redroid-data 2>/dev/null || true
bun run emu:prepare
bun run emu:up
bun run emu:wait
adb -s localhost:5555 shell wm size   # should print Physical size: 1080x1920

Browser mirror (http://localhost:8000) is best-effort — the packaged ws-scrcpy server can choke on newer Android clipboard APIs. Reliable viewer on the Mac:

brew install scrcpy
scrcpy -s localhost:5555 --no-audio
bun run emu:doctor
bun run emu:up && bun run emu:wait && bun run emu:adb
adb -s localhost:5555 shell getprop ro.build.version.release
  • Prefer adb -s localhost:5555 so aliases like emulator-5554 do not confuse tooling.
  • Pairing still needs a human (or browser operator) for the six-digit code.
  • docker/android/compose.yml + scripts/emu are the source of truth.