lib/nerves/utils/stream.ex

defmodule Nerves.Utils.Stream do
  use GenServer

  @timer 10_000

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def stop(pid) do
    GenServer.stop(pid)
  end

  def history(pid) do
    GenServer.call(pid, :history)
  end

  def init(opts) do
    file = opts[:file]
    history_lines = opts[:history_lines] || 100

    if file != nil do
      File.write(file, "", [:write])
    end

    {:ok,
     %{
       file: opts[:file],
       timer: Process.send_after(self(), :keep_alive, @timer),
       history: :queue.new(),
       history_lines: history_lines,
       history_saved: 0
     }}
  end

  def handle_call(:history, _from, s) do
    history =
      s.history
      |> :queue.to_list()
      |> Enum.join()

    {:reply, history, s}
  end

  def handle_info({:io_request, from, reply_as, {:put_chars, _encoding, chars}} = data, s) do
    if s.file != nil do
      File.write(s.file, chars, [:append])
    end

    s = save_history(s, chars)
    reply(from, reply_as, :ok)
    {:noreply, stdout(chars, data, s)}
  end

  def handle_info(:keep_alive, s) do
    IO.write(".")
    {:noreply, reset_timer(s)}
  end

  def stdout(<<">>>", tail::binary>>, _message, s), do: trim_write(">>>", "\n", tail, s)
  def stdout(<<"\e[7m>>>", tail::binary>>, _message, s), do: trim_write(">>>", "\e[7m", tail, s)

  def stdout(_, _, s), do: s

  defp trim_write(trim, split, bin, s) do
    IO.write("\n")

    [bin | _] =
      bin
      |> String.split(trim)

    (trim <> bin)
    |> String.split(split)
    |> List.first()
    |> String.trim()
    |> IO.write()

    reset_timer(s)
  end

  defp reset_timer(s) do
    Process.cancel_timer(s.timer)
    %{s | timer: Process.send_after(self(), :keep_alive, @timer)}
  end

  defp save_history(%{history_saved: lines, history_lines: lines} = s, line) do
    history = :queue.in(line, s.history)
    {_, history} = :queue.out(history)
    %{s | history: history}
  end

  defp save_history(s, line) do
    history = :queue.in(line, s.history)
    %{s | history: history, history_saved: s.history_saved + 1}
  end

  def reply(from, reply_as, reply) do
    send(from, {:io_reply, reply_as, reply})
  end
end