lib/mix_test_interactive/command_processor.ex

defmodule MixTestInteractive.CommandProcessor do
  @moduledoc """
  Processes interactive mode commands.
  """

  alias MixTestInteractive.{Command, Settings}

  alias MixTestInteractive.Command.{
    AllTests,
    Failed,
    Help,
    Pattern,
    Quit,
    RunTests,
    Stale,
    ToggleWatchMode
  }

  @type response :: Command.response()

  @commands [
    Pattern,
    Stale,
    Failed,
    AllTests,
    ToggleWatchMode,
    RunTests,
    Help,
    Quit
  ]

  @doc """
  Processes a single interactive mode command.
  """
  @spec call(String.t() | :eof, Settings.t()) :: response()
  def call(:eof, _settings), do: :quit

  def call(command_line, settings) when is_binary(command_line) do
    case String.split(command_line) do
      [] -> process_command("", [], settings)
      [command | args] -> process_command(command, args, settings)
    end
  end

  @doc """
  Returns an ANSI-formatted usage summary.

  Includes only commands that are applicable to the current configuration.
  """
  @spec usage(Settings.t()) :: IO.chardata()
  def usage(settings) do
    usage =
      settings
      |> applicable_commands()
      |> Enum.flat_map(&usage_line/1)

    ([:bright, "Usage:\n", :normal] ++ usage)
    |> IO.ANSI.format()
  end

  defp usage_line(command) do
    ["› ", :bright, command.name, :normal, " to ", command.description, ".\n"]
    |> IO.ANSI.format_fragment()
  end

  defp process_command(command, args, settings) do
    case settings
         |> applicable_commands()
         |> Enum.find(nil, &(&1.command == command)) do
      nil -> :unknown
      cmd -> cmd.run(args, settings)
    end
  end

  defp applicable_commands(settings) do
    Enum.filter(@commands, & &1.applies?(settings))
  end
end