4/27/2026 at 1:32:18 AM
Same, I've added a .#screenshots derivation. High up-front effort but almost zero maintenance afterwards.Bonus: since you're generating screenshots programmatically anyway, you can generate a pair of each with your app's light/dark theme, and swap them in/out depending on prefers-color-scheme: dark. <picture> elements work in GitHub READMEs, too: https://github.com/CyberShadow/CyDo#readme
by CyberShadow
4/27/2026 at 7:59:09 AM
+1 for this approach. For a mobile app, I made Nix spawn an ephemeral Android emulator instance for generating up-to-date screenshots, requiring no prior setup and leaving no lingering data around after running. Setting it up wasn't that high-effort in my case either; coming up with the idea was the hard part, the Nix code was one-shot by your favorite LLM.Granted manually updating the screenshots isn't the most laborious task in the world, but the "upload-apk + take-screenshot + transfer-back-to-PC + edit" process is usually barely annoying enough that you end up almost never doing it otherwise (similar to the OP's experience in the closing paragraph).
by neobrain
4/28/2026 at 5:36:05 PM
That sounds so cool! Is the repo available anywhere?by Landing7610
5/3/2026 at 5:05:24 PM
Nothing public yet, but this is the Nix output for taking the screenshot, to be executed via `nix run .#screenshot`: outputs.apps.x86_64.screenshot = {
type = "app";
program = toString (pkgs.writeShellScript "screenshot-script" ''
set -euo pipefail
EMU_SDK="${androidEmulatorComposition.androidsdk}/libexec/android-sdk"
ADB="$EMU_SDK/platform-tools/adb"
EMULATOR="$EMU_SDK/emulator/emulator"
APK="${self.packages.${system}.debug}/myapp-debug.apk"
SRC_DIR="$(${pkgs.git}/bin/git rev-parse --show-toplevel)"
AVD_HOME="$(mktemp -d)"
trap 'kill "$EMU_PID" 2>/dev/null; wait "$EMU_PID" 2>/dev/null; rm -rf "$AVD_HOME"' EXIT
# Create AVD
AVD_DIR="$AVD_HOME/screenshot.avd"
mkdir -p "$AVD_DIR"
cat > "$AVD_HOME/screenshot.ini" <<EOF
avd.ini.encoding=UTF-8
path=$AVD_DIR
target=android-${platformVersion}
EOF
cat > "$AVD_DIR/config.ini" <<EOF
AvdId=screenshot
PlayStore.enabled=false
abi.type=x86_64
avd.ini.encoding=UTF-8
hw.cpu.arch=x86_64
hw.gpu.enabled=yes
hw.gpu.mode=swiftshader_indirect
hw.lcd.density=420
hw.lcd.height=2400
hw.lcd.width=1080
hw.ramSize=2048
image.sysdir.1=system-images/android-${platformVersion}/google_apis/x86_64/
skin.dynamic=yes
tag.display=Google APIs
tag.id=google_apis
disk.dataPartition.size=2G
EOF
echo "==> Starting emulator..."
ANDROID_AVD_HOME="$AVD_HOME" ANDROID_HOME="$EMU_SDK" \
"$EMULATOR" -avd screenshot -no-window -no-audio -no-boot-anim \
-gpu swiftshader_indirect -no-snapshot 2>&1 &
EMU_PID=$!
echo "==> Waiting for boot..."
for i in $(seq 1 90); do
BOOT=$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') || true
if [ "$BOOT" = "1" ]; then
echo " Booted after ~$((i * 2))s"
break
fi
sleep 2
done
if [ "$BOOT" != "1" ]; then
echo "ERROR: Emulator failed to boot" >&2
exit 1
fi
# Enable dark mode
"$ADB" shell cmd uimode night yes
# Install and launch
echo "==> Installing APK..."
"$ADB" install -r "$APK"
"$ADB" shell pm grant com.me.myapp android.permission.WRITE_SECURE_SETTINGS
"$ADB" shell am start -n com.me.myapp/.MainActivity
sleep 3
# Navigate to settings screen by tapping "Notification Filters" button
# This uses uiautomator to find the button by text for robustness
"$ADB" shell uiautomator dump /sdcard/ui.xml
BOUNDS=$("$ADB" shell cat /sdcard/ui.xml \
| ${pkgs.gnugrep}/bin/grep -oP 'text="Notification Filters"[^>]*bounds="\K[^"]+' \
|| true)
if [ -z "$BOUNDS" ]; then
echo "ERROR: Could not find Notification Filters button" >&2
exit 1
fi
# Parse bounds "[x1,y1][x2,y2]" to compute center tap coordinates
X1=$(echo "$BOUNDS" | ${pkgs.gnused}/bin/sed 's/\[\([0-9]*\),\([0-9]*\)\]\[\([0-9]*\),\([0-9]*\)\]/\1/')
Y1=$(echo "$BOUNDS" | ${pkgs.gnused}/bin/sed 's/\[\([0-9]*\),\([0-9]*\)\]\[\([0-9]*\),\([0-9]*\)\]/\2/')
X2=$(echo "$BOUNDS" | ${pkgs.gnused}/bin/sed 's/\[\([0-9]*\),\([0-9]*\)\]\[\([0-9]*\),\([0-9]*\)\]/\3/')
Y2=$(echo "$BOUNDS" | ${pkgs.gnused}/bin/sed 's/\[\([0-9]*\),\([0-9]*\)\]\[\([0-9]*\),\([0-9]*\)\]/\4/')
TAP_X=$(( (X1 + X2) / 2 ))
TAP_Y=$(( (Y1 + Y2) / 2 ))
"$ADB" shell input tap "$TAP_X" "$TAP_Y"
sleep 2
# Capture and process screenshot
echo "==> Capturing screenshot..."
"$ADB" shell screencap -p /sdcard/screenshot.png
"$ADB" pull /sdcard/screenshot.png "$AVD_HOME/raw.png"
# Crop to content: remove status bar (top 128px) and empty space below
# Per-App Overrides, then resize with high-quality Lanczos filter
${pkgs.imagemagick}/bin/magick "$AVD_HOME/raw.png" \
-crop 1080x1100+0+128 +repage \
-filter Lanczos -resize 540x \
"$SRC_DIR/fastlane/metadata/android/en-US/images/phoneScreenshots/settings.png"
echo "==> Screenshot saved to fastlane/metadata/android/en-US/images/phoneScreenshots/settings.png"
'');
};
by neobrain
4/27/2026 at 12:30:04 PM
The <picture> in README trick works like magic. Thank you! I'm going to steal it.by 9dev