Thank you very much, this seems to have solved it. As you suggested, I simply replaced my IO.gets/1 call with a get_until call.
For clarity for anyone else reading this, here is the code change, replacing IO.gets/1
Before:
defp get_input(message, %{input_type: :visible}) do
ensure_string_result(message, &IO.gets/1)
end
After:
def gets(device \\ :stdio, prompt) do
:io.request(map_dev(device), {:get_until, :unicode, to_chardata(prompt), __MODULE__, :__parse__, []})
end
defp to_chardata(list) when is_list(list), do: list
defp to_chardata(other), do: to_string(other)
@doc false
def __parse__([], :eof), do: {:done, :eof, []}
def __parse__([], chars), do: {:done, List.to_string(chars), []}
# Map the Elixir names for standard IO and error to Erlang names
defp map_dev(:stdio), do: :standard_io
defp map_dev(:stderr), do: :standard_error
defp map_dev(other) when is_atom(other) or is_pid(other) or is_tuple(other), do: other
defp get_input(message, %{input_type: :visible}) do
ensure_string_result(message, &gets/1)
end