Packages
mob_dev
0.6.21
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/mob.emulators.ex
defmodule Mix.Tasks.Mob.Emulators do
use Mix.Task
@shortdoc "List, start, and stop Android emulators / iOS simulators"
@moduledoc """
Manage virtual devices: Android emulators (AVDs) and iOS simulators.
## Examples
mix mob.emulators # list all (default)
mix mob.emulators --list # same as above
mix mob.emulators --list --android # Android only
mix mob.emulators --list --ios # iOS only
mix mob.emulators --start --id Pixel_8_API_34
mix mob.emulators --start --id 78354490
mix mob.emulators --stop --id emulator-5554
mix mob.emulators --stop --id 78354490
mix mob.emulators --stop --all # everything booted
`--id` accepts the same display IDs `mix mob.devices` shows, plus AVD
names. For Android the running serial (`emulator-5554`) also works.
Out of scope: creating new AVDs or installing simulator runtimes — those
involve license acceptance and multi-GB downloads. Use Android Studio /
Xcode for that.
"""
alias MobDev.{Device, Emulators}
@switches [
list: :boolean,
start: :boolean,
stop: :boolean,
android: :boolean,
ios: :boolean,
id: :string,
all: :boolean
]
@impl Mix.Task
def run(args) do
{opts, _, _} = OptionParser.parse(args, switches: @switches)
cond do
opts[:start] -> do_start(opts)
opts[:stop] -> do_stop(opts)
true -> do_list(opts)
end
end
# ── List ──────────────────────────────────────────────────────────────────
defp do_list(opts) do
# Both shown if neither flag specified, otherwise only the requested one(s).
android? = Keyword.get(opts, :android, false)
ios? = Keyword.get(opts, :ios, false)
# `or` short-circuits, so the right operand only evaluates when the
# left is false — at which point `not android?` / `not ios?` is
# redundant and Elixir 1.20's type checker flags it. Simplified:
# show the unrequested one only when the requested one isn't asked
# for at all (i.e. no flags → show both).
show_android = android? or not ios?
show_ios = ios? or not android?
IO.puts("")
if show_android do
print_android_section()
IO.puts("")
end
if show_ios do
print_ios_section()
IO.puts("")
end
end
defp print_android_section do
IO.puts("#{cyan()}Android emulators (AVDs)#{reset()}")
case Emulators.list_android() do
{:ok, []} ->
IO.puts(" (no AVDs configured — create one in Android Studio)")
{:ok, avds} ->
Enum.each(avds, &print_avd/1)
{:error, reason} ->
IO.puts(" #{yellow()}#{reason}#{reset()}")
end
end
defp print_ios_section do
IO.puts("#{cyan()}iOS simulators#{reset()}")
case Emulators.list_ios() do
{:ok, sims} ->
# Group by runtime and sort booted-first within each group.
sims
|> Enum.sort_by(&{&1.runtime, not &1.running, &1.name})
|> Enum.each(&print_sim/1)
{:error, reason} ->
IO.puts(" #{yellow()}#{reason}#{reset()}")
end
end
defp print_avd(%Emulators{platform: :android} = a) do
dot = if a.running, do: "#{green()}●#{reset()}", else: "○"
suffix = if a.running, do: " #{dim()}(running, #{a.serial})#{reset()}", else: ""
IO.puts(" #{dot} #{bold()}#{a.name}#{reset()}#{suffix}")
end
defp print_sim(%Emulators{platform: :ios} = s) do
dot = if s.running, do: "#{green()}●#{reset()}", else: "○"
state = if s.running, do: "booted, ", else: ""
short_id = String.replace(s.id, "-", "") |> String.slice(0, 8) |> String.downcase()
IO.puts(
" #{dot} #{bold()}#{pad(s.name, 28)}#{reset()} #{s.runtime} #{dim()}(#{state}#{short_id})#{reset()}"
)
end
# ── Start ─────────────────────────────────────────────────────────────────
defp do_start(opts) do
id = opts[:id]
if is_nil(id) do
Mix.raise("--start requires --id <id>. See `mix mob.emulators --list` for IDs.")
end
case resolve(id) do
{:android, %Emulators{name: avd_name, running: false}} ->
IO.puts("Starting Android emulator: #{avd_name}")
case Emulators.start_android(avd_name) do
:ok ->
IO.puts(
"#{green()}Started.#{reset()} (boots in background — `adb wait-for-device` to block)"
)
{:error, reason} ->
Mix.raise(reason)
end
{:android, %Emulators{name: avd_name, running: true, serial: serial}} ->
IO.puts("Already running: #{avd_name} (#{serial})")
{:ios, %Emulators{name: name, id: udid, running: false}} ->
IO.puts("Booting iOS simulator: #{name}")
case Emulators.start_ios(udid) do
:ok -> IO.puts("#{green()}Booted.#{reset()}")
{:error, reason} -> Mix.raise(reason)
end
{:ios, %Emulators{name: name, running: true}} ->
IO.puts("Already booted: #{name}")
:not_found ->
Mix.raise("No emulator/simulator matched #{inspect(id)}. Run `mix mob.emulators --list`.")
end
end
# ── Stop ──────────────────────────────────────────────────────────────────
defp do_stop(opts) do
cond do
opts[:all] ->
do_stop_all(opts)
opts[:id] ->
do_stop_one(opts[:id])
true ->
Mix.raise(
"--stop needs either --id <id> or --all. " <>
"Use --all to stop every running emulator/simulator."
)
end
end
defp do_stop_one(id) do
case resolve(id) do
{:android, %Emulators{running: true, serial: serial, name: name}} ->
IO.puts("Stopping Android emulator: #{name} (#{serial})")
case Emulators.stop_android(serial) do
:ok -> IO.puts("#{green()}Stopped.#{reset()}")
{:error, reason} -> Mix.raise(reason)
end
{:android, %Emulators{running: false, name: name}} ->
IO.puts("Not running: #{name}")
{:ios, %Emulators{running: true, id: udid, name: name}} ->
IO.puts("Shutting down iOS simulator: #{name}")
case Emulators.stop_ios(udid) do
:ok -> IO.puts("#{green()}Stopped.#{reset()}")
{:error, reason} -> Mix.raise(reason)
end
{:ios, %Emulators{running: false, name: name}} ->
IO.puts("Not booted: #{name}")
:not_found ->
Mix.raise("No emulator/simulator matched #{inspect(id)}. Run `mix mob.emulators --list`.")
end
end
defp do_stop_all(opts) do
# Both shown if neither flag specified, otherwise only the requested one(s).
android? = Keyword.get(opts, :android, false)
ios? = Keyword.get(opts, :ios, false)
# `or` short-circuits, so the right operand only evaluates when the
# left is false — at which point `not android?` / `not ios?` is
# redundant and Elixir 1.20's type checker flags it. Simplified:
# show the unrequested one only when the requested one isn't asked
# for at all (i.e. no flags → show both).
show_android = android? or not ios?
show_ios = ios? or not android?
running =
[]
|> then(fn acc ->
if show_android do
case Emulators.list_android() do
{:ok, avds} -> acc ++ Enum.filter(avds, & &1.running)
_ -> acc
end
else
acc
end
end)
|> then(fn acc ->
if show_ios do
case Emulators.list_ios() do
{:ok, sims} -> acc ++ Enum.filter(sims, & &1.running)
_ -> acc
end
else
acc
end
end)
if running == [] do
IO.puts("No running emulators or simulators.")
else
names = Enum.map_join(running, ", ", & &1.name)
IO.puts("Stopping #{length(running)} running: #{names}")
Enum.each(running, fn
%Emulators{platform: :android, serial: serial, name: name} ->
case Emulators.stop_android(serial) do
:ok -> IO.puts(" #{green()}✓#{reset()} #{name}")
{:error, reason} -> IO.puts(" #{red()}✗#{reset()} #{name}: #{reason}")
end
%Emulators{platform: :ios, id: udid, name: name} ->
case Emulators.stop_ios(udid) do
:ok -> IO.puts(" #{green()}✓#{reset()} #{name}")
{:error, reason} -> IO.puts(" #{red()}✗#{reset()} #{name}: #{reason}")
end
end)
end
end
# ── Resolution ────────────────────────────────────────────────────────────
# Try Android first then iOS — they don't share id formats so collisions
# are vanishingly rare. Match against the AVD name (Android), the running
# adb serial (Android), the UDID (iOS), or the 8-char display id (iOS).
defp resolve(id) do
android_match =
case Emulators.list_android() do
{:ok, avds} -> Enum.find(avds, &android_id_match?(&1, id))
_ -> nil
end
if android_match do
{:android, android_match}
else
case Emulators.list_ios() do
{:ok, sims} ->
case Enum.find(sims, &ios_id_match?(&1, id)) do
nil -> :not_found
sim -> {:ios, sim}
end
_ ->
:not_found
end
end
end
defp android_id_match?(%Emulators{name: name, serial: serial}, id) do
String.downcase(name) == String.downcase(id) or
(serial != nil and String.downcase(serial) == String.downcase(id))
end
defp ios_id_match?(%Emulators{id: udid}, id) do
# Build a fake Device just to reuse Device.match_id?/2's "display_id or serial"
# logic. Simulator display_id = first 8 hex chars of UDID with dashes removed.
fake = %Device{platform: :ios, type: :simulator, serial: udid}
Device.match_id?(fake, id)
end
# ── ANSI helpers ──────────────────────────────────────────────────────────
defp cyan, do: IO.ANSI.cyan()
defp green, do: IO.ANSI.green()
defp yellow, do: IO.ANSI.yellow()
defp red, do: IO.ANSI.red()
defp bold, do: IO.ANSI.bright()
defp dim, do: IO.ANSI.faint()
defp reset, do: IO.ANSI.reset()
defp pad(s, n) do
pad_len = max(n - String.length(s), 0)
s <> String.duplicate(" ", pad_len)
end
end