twonly-app/scripts/run_emulators.sh
2026-08-30 09:25:15 +02:00

163 lines
5 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Start `flutter run` for every currently running iOS/Android emulator,
# each in its own zellij tab of a fresh session, so the logs stay separate.
#
# Usage: scripts/run_emulators.sh [-s SESSION] [-w SECONDS] [-f] [-- <extra flutter run args>]
#
# -s SESSION zellij session name (default: twonly-run)
# -w SECONDS stagger between device starts, avoids build-dir races (default: 15)
# -f kill an existing session with the same name instead of aborting
#
# Everything after `--` is appended to each `flutter run` invocation,
# e.g. `scripts/run_emulators.sh -- --flavor dev --dart-define=FOO=bar`
set -euo pipefail
SESSION="twonly-run"
STAGGER=15
FORCE=0
while getopts ":s:w:fh" opt; do
case "$opt" in
s) SESSION="$OPTARG" ;;
w) STAGGER="$OPTARG" ;;
f) FORCE=1 ;;
h) sed -n '2,15p' "$0"; exit 0 ;;
\?) echo "unknown option: -$OPTARG" >&2; exit 2 ;;
:) echo "option -$OPTARG needs an argument" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))
case "$STAGGER" in
''|*[!0-9]*) echo "-w needs a non-negative whole number of seconds" >&2; exit 2 ;;
esac
for bin in zellij flutter jq; do
command -v "$bin" >/dev/null || { echo "missing required tool: $bin" >&2; exit 1; }
done
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ZELLIJ_CONFIG="${ZELLIJ_CONFIG_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/zellij/config.kdl}"
if zellij list-sessions -s 2>/dev/null | grep -qx "$SESSION"; then
if [ "$FORCE" -eq 1 ]; then
echo "deleting existing session '$SESSION'..."
zellij delete-session --force "$SESSION" >/dev/null
else
echo "zellij session '$SESSION' already exists. Attach with:" >&2
echo " zellij attach $SESSION" >&2
echo "or re-run with -f to replace it." >&2
exit 1
fi
fi
echo "querying flutter devices..."
DEVICES_JSON="$(flutter devices --machine)"
# Only running emulators/simulators on the ios or android platforms.
# bash 3.2 (macOS default) has no mapfile, so read the lines manually.
DEVICES=()
while IFS= read -r line; do
[ -n "$line" ] && DEVICES[${#DEVICES[@]}]="$line"
done < <(
printf '%s' "$DEVICES_JSON" | jq -r '
.[]
| select(.emulator == true and .isSupported == true)
| select(.targetPlatform | test("^(ios|android)"))
| "\(.id)\t\(.name)\t\(.targetPlatform)"
'
)
if [ "${#DEVICES[@]}" -eq 0 ]; then
echo "no running iOS or Android emulators found." >&2
echo "start one first, e.g. 'flutter emulators --launch <id>' or open Simulator.app." >&2
exit 1
fi
RUNDIR="$(mktemp -d "${TMPDIR:-/tmp}/twonly-run-XXXXXX")"
LAYOUT="$RUNDIR/layout.kdl"
# A custom layout replaces zellij's default UI, so re-declare the default tab
# template -- without it the tab bar and status bar are gone and the tabs,
# although they exist, are invisible and unswitchable-looking.
if grep -qE '^[[:space:]]*default_layout[[:space:]]+"compact"' "$ZELLIJ_CONFIG" 2>/dev/null; then
TAB_TEMPLATE=' default_tab_template {
children
pane size=1 borderless=true {
plugin location="zellij:compact-bar"
}
}'
else
TAB_TEMPLATE=' default_tab_template {
pane size=1 borderless=true {
plugin location="zellij:tab-bar"
}
children
pane size=2 borderless=true {
plugin location="zellij:status-bar"
}
}'
fi
{
echo 'layout {'
echo "$TAB_TEMPLATE"
} > "$LAYOUT"
index=0
USED_NAMES=()
for entry in "${DEVICES[@]}"; do
id="${entry%% *}"
rest="${entry#* }"
name="${rest%% *}"
platform="${rest#* }"
# Two clones of the same emulator image share a name, so identify Android
# tabs by their (unique) adb id and fall back to a counter elsewhere.
case "$platform" in
ios*) label="ios-$name" ;;
*) label="android-$id" ;;
esac
# tab names must survive KDL quoting; keep them boring
tab_name="$(printf '%s' "$label" | tr -c 'A-Za-z0-9._-' '-' | cut -c1-30)"
suffix=2
while printf '%s\n' ${USED_NAMES+"${USED_NAMES[@]}"} | grep -qx "$tab_name"; do
tab_name="$(printf '%s' "$label" | tr -c 'A-Za-z0-9._-' '-' | cut -c1-27)-$suffix"
suffix=$((suffix + 1))
done
USED_NAMES[${#USED_NAMES[@]}]="$tab_name"
wrapper="$RUNDIR/run-$index.sh"
{
echo '#!/usr/bin/env bash'
printf 'cd %q\n' "$PROJECT_ROOT"
if [ "$index" -gt 0 ] && [ "$STAGGER" -gt 0 ]; then
delay=$((index * STAGGER))
printf 'echo "waiting %ss so the builds do not fight over the build directory..."\n' "$delay"
printf 'sleep %s\n' "$delay"
fi
printf 'echo "=== %s (%s) ==="\n' "$name" "$id"
printf 'exec flutter run -d %q' "$id"
for arg in "$@"; do printf ' %q' "$arg"; done
printf '\n'
} > "$wrapper"
chmod +x "$wrapper"
{
printf ' tab name="%s" {\n' "$tab_name"
printf ' pane command="bash" {\n'
printf ' args "%s"\n' "$wrapper"
printf ' }\n'
printf ' }\n'
} >> "$LAYOUT"
echo " tab '$tab_name' -> $id"
index=$((index + 1))
done
echo '}' >> "$LAYOUT"
echo "starting zellij session '$SESSION' with $index tab(s)..."
exec zellij --session "$SESSION" --new-session-with-layout "$LAYOUT"