lib/line_update_terminal_message.ex

defmodule BuildPipeline.LineUpdateTerminalMessage do
  alias IO.ANSI

  def build(%{line_update: true} = message, server_state) do
    %{ansi_prefix: ansi_prefix, prefix: prefix, suffix: suffix, runner_pid: runner_pid} = message
    %{runners: runners, terminal_width: terminal_width} = server_state
    %{terminal_line_number: line_number} = Map.fetch!(runners, runner_pid)

    message = combine_onto_one_line(prefix, suffix, terminal_width)
    wrapped_message = "#{ansi_prefix}#{message}#{ANSI.reset()}"

    max_lines = max_runner_output_lines(runners)

    line_shift = max_lines - line_number + 1

    "\r#{ANSI.cursor_up(line_shift)}\r#{ANSI.clear_line()}#{wrapped_message}#{ANSI.cursor_down(line_shift)}\r"
  end

  defp combine_onto_one_line(prefix, suffix, max_length) do
    prefix_len = String.length(prefix)
    suffix_len = String.length(suffix)

    if prefix_len + suffix_len + 1 < max_length do
      prefix <> " " <> suffix
    else
      acc_single_line_message(prefix, suffix, max_length)
    end
  end

  defp acc_single_line_message(prefix, suffix, max_length) do
    prefix = String.graphemes(prefix)
    suffix = [" ", ".", ".", ".", " " | String.graphemes(suffix)]
    truncated_suffix_len = length(suffix)

    allowed_prefix_len = max_length - length(suffix)

    if truncated_suffix_len >= max_length do
      Enum.take(prefix, max_length)
    else
      Enum.join(Enum.take(prefix, allowed_prefix_len) ++ suffix)
    end
  end

  defp max_runner_output_lines(runners) do
    Enum.reduce(runners, 0, fn {_pid, %{terminal_line_number: line_number}}, acc ->
      if line_number >= acc do
        line_number
      else
        acc
      end
    end)
  end
end