Current section
Files
Jump to
Current section
Files
lib/serum/io_proxy.ex
defmodule Serum.IOProxy do
@moduledoc false
use GenServer
message_categories = [
debug: {[:light_black_background, :black], [:light_black]},
error: {[:red_background, :light_white], [:light_white]},
gen: {[:light_green], []},
info: {[:light_black_background, :white], []},
mkdir: {[:light_cyan], []},
plugin: {[:light_magenta], []},
read: {[:light_yellow], []},
theme: {[:light_magenta], []},
warn: {[:yellow_background, :black], [:yellow]}
]
@type config :: %{mute_msg: boolean(), mute_err: boolean()}
@spec start_link(term()) :: GenServer.on_start()
def start_link(_args) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc "Gets the current configuration of `Serum.IOProxy`."
@spec config() :: {:ok, config()}
def config do
GenServer.call(__MODULE__, :get_config)
end
@doc """
Configures `Serum.IOProxy`.
## Options
- `mute_err` (boolean): Controls whether outputs to `:stderr` must be
suppressed. Defaults to `false`.
- `mute_msg` (boolean): Controls whether outputs to `:stdio` must be
suppressed. Defaults to `false`.
"""
@spec config(keyword()) :: :ok
def config(options) when is_list(options) do
GenServer.call(__MODULE__, {:set_config, options})
end
@doc """
Prints a message to the standard output.
Available categories are:
`#{message_categories |> Keyword.keys() |> inspect()}`
"""
@spec put_msg(atom(), IO.ANSI.ansidata()) :: :ok
def put_msg(category, msg) do
GenServer.call(__MODULE__, {:put_msg, category, msg})
end
@doc """
Prints a message to the standard error output.
Available categories are:
`#{message_categories |> Keyword.keys() |> inspect()}`
"""
@spec put_err(atom(), IO.ANSI.ansidata()) :: :ok
def put_err(category, msg) do
GenServer.call(__MODULE__, {:put_err, category, msg})
end
@impl GenServer
def init(_args) do
prefix = if IO.ANSI.enabled?(), do: "\r", else: ""
{:ok, %{mute_msg: false, mute_err: false, prefix: prefix}}
end
@impl GenServer
def handle_call(request, from, state)
def handle_call(:get_config, _, state), do: {:reply, {:ok, state}, state}
def handle_call({:set_config, conf}, _, state) do
conf_map = conf |> Map.new() |> Map.take([:mute_msg, :mute_err])
{:reply, :ok, Map.merge(state, conf_map)}
end
def handle_call({:put_msg, category, msg}, _, state) do
unless state.mute_msg do
IO.puts([state.prefix, format_message(category, msg)])
end
{:reply, :ok, state}
end
def handle_call({:put_err, category, msg}, _, state) do
unless state.mute_err do
IO.puts(:stderr, [state.prefix, format_message(category, msg)])
end
{:reply, :ok, state}
end
max_length =
message_categories
|> Enum.map(fn {category, _} -> to_string(category) end)
|> Enum.map(&String.length/1)
|> Enum.max()
@spec format_message(atom(), IO.ANSI.ansidata()) :: IO.chardata()
defp format_message(category, msg)
Enum.each(message_categories, fn {category, {head_style, body_style}} ->
cat_str = category |> to_string() |> String.upcase()
header = [head_style, String.pad_leading(cat_str, max_length + 1), ?\s]
body_fmt = [body_style, "~ts"]
defp format_message(unquote(category), msg) do
header = unquote(header) |> IO.ANSI.format() |> IO.iodata_to_binary()
body_fmt = unquote(body_fmt) |> IO.ANSI.format() |> IO.iodata_to_binary()
formatted_msg =
msg
|> IO.ANSI.format()
|> IO.iodata_to_binary()
|> format_newlines()
[header, ?\s, :io_lib.format(body_fmt, [formatted_msg])]
end
end)
@spec format_newlines(binary()) :: IO.chardata()
defp format_newlines(msg) do
lines = String.split(msg, ~r/\r?\n/)
indent = String.duplicate(" ", unquote(max_length) + 3)
Enum.intersperse(lines, ["\n", indent])
end
end