"""
end
defp render_input(arg, form_values) do
value = Map.get(form_values, arg.name, arg.default)
case arg.type do
"boolean" ->
assigns = %{arg: arg, checked: value == true or value == "true"}
~H"""
"""
"enum:" <> values_str ->
values = parse_enum_values(values_str)
assigns = %{arg: arg, values: values, value: value}
~H"""
"""
type when type in ["integer", "float"] ->
step = if type == "float", do: "0.01", else: "1"
assigns = %{arg: arg, value: value, step: step}
~H"""
"""
"atom" ->
# Atom values render with the leading `:` so the submitted form value
# round-trips back through `parse_value/1`'s atom branch and reaches
# the handler as an actual atom. Without this, a `default: :foo`
# initially renders as the bare string `"foo"`, the user submits a
# bare string, and `is_atom/1` guards on the handler fail.
assigns = %{arg: arg, value: atom_input_value(value)}
~H"""
"""
_ ->
assigns = %{arg: arg, value: value}
~H"""
"""
end
end
defp atom_input_value(value) when is_atom(value) and not is_nil(value),
do: ":" <> Atom.to_string(value)
defp atom_input_value(":" <> _ = value), do: value
defp atom_input_value(value) when is_binary(value) and byte_size(value) > 0,
do: ":" <> value
defp atom_input_value(_), do: ""
defp parse_enum_values(values_str) do
values_str
|> String.trim_leading("[")
|> String.trim_trailing("]")
|> String.split(", ")
|> Enum.map(&String.trim/1)
end
@impl Phoenix.LiveComponent
def handle_event("select_tab", %{"tab" => tab}, socket) do
tab_atom = String.to_existing_atom(tab)
{:noreply, assign(socket, active_tab: tab_atom, result: nil, error: nil)}
end
def handle_event("execute", %{"command" => cmd_name, "args" => args}, socket) do
execute_command(socket, cmd_name, args)
end
def handle_event("execute", %{"command" => cmd_name}, socket) do
execute_command(socket, cmd_name, %{})
end
def handle_event("cancel", _params, socket) do
if pid = socket.assigns.command_pid, do: BB.Command.cancel(pid)
{:noreply, socket}
end
defp execute_command(socket, cmd_name, args) do
cmd_atom = String.to_existing_atom(cmd_name)
if valid_robot?(socket.assigns.robot_module) do
parsed_args = parse_args(args)
# Stash the submitted form values per-command so the next render after
# the command completes shows what the user typed rather than reverting
# to declared defaults. Keys come back from the form as strings; convert
# them to atoms so render_input's `Map.get(form_values, arg.name, ...)`
# lookups hit.
args_by_atom_key = Map.new(args, fn {k, v} -> {String.to_existing_atom(k), v} end)
form_values = Map.put(socket.assigns.form_values, cmd_atom, args_by_atom_key)
socket =
socket
|> assign(:form_values, form_values)
|> assign(:result, nil)
|> assign(:error, nil)
case RobotRuntime.execute(socket.assigns.robot_module, cmd_atom, parsed_args) do
{:ok, pid} ->
await_command(socket, pid)
{:noreply,
socket
|> assign(:executing, cmd_atom)
|> assign(:command_pid, pid)}
{:error, reason} ->
{:noreply, assign(socket, :error, inspect(reason))}
end
else
{:noreply, assign(socket, :error, "No valid robot connected")}
end
end
defp parse_args(args) do
args
|> Enum.map(fn {k, v} -> {String.to_existing_atom(k), parse_value(v)} end)
|> Map.new()
end
# Await with `:infinity` rather than a UI-side deadline: a continuous command
# only returns when it stops or is cancelled, and the command's own DSL
# `timeout` already bounds runaways. The user-facing escape hatch is the
# Cancel button, which stops the command and resolves this await.
defp await_command(socket, pid) do
liveview_pid = socket.root_pid
component_id = socket.assigns.id
Task.start(fn ->
result = BB.Command.await(pid, :infinity)
notify_command_complete(liveview_pid, component_id, result)
end)
end
defp notify_command_complete(liveview_pid, component_id, {:ok, result}) do
send(liveview_pid, {:command_complete, component_id, {:ok, inspect(result)}})
end
defp notify_command_complete(liveview_pid, component_id, {:ok, result, _metadata}) do
send(liveview_pid, {:command_complete, component_id, {:ok, inspect(result)}})
end
defp notify_command_complete(liveview_pid, component_id, {:error, reason}) do
send(liveview_pid, {:command_complete, component_id, {:error, inspect(reason)}})
end
defp parse_value("true"), do: true
defp parse_value("false"), do: false
defp parse_value(":" <> rest = value) when byte_size(rest) > 0 do
String.to_existing_atom(rest)
rescue
ArgumentError -> value
end
defp parse_value(value) when is_binary(value) do
case Integer.parse(value) do
{int, ""} ->
int
_ ->
case Float.parse(value) do
{float, ""} -> float
_ -> value
end
end
end
defp parse_value(value), do: value
defp discover_commands(robot_module) do
robot_module
|> DslInfo.commands()
|> Enum.map(&format_command/1)
|> Enum.sort_by(& &1.name)
end
defp format_command(cmd) do
%{
name: cmd.name,
handler: cmd.handler,
timeout: cmd.timeout,
allowed_states: cmd.allowed_states,
arguments: Enum.map(cmd.arguments, &format_argument/1)
}
end
defp format_argument(arg) do
%{
name: arg.name,
type: format_type(arg.type),
required: arg.required,
default: arg.default,
doc: arg.doc
}
end
defp format_type(type) when is_atom(type), do: Atom.to_string(type)
defp format_type({:in, values}), do: "enum:#{inspect(values)}"
defp format_type(type), do: inspect(type)
defp valid_robot?(robot_module) when is_atom(robot_module) do
function_exported?(robot_module, :robot, 0) and
function_exported?(robot_module, :spark_dsl_config, 0)
end
defp valid_robot?(_), do: false
end