defmodule PacmanProgressBar do
@moduledoc """
This module creates a progress bar based on Arch Linux's pacman package manager
"""
@initial_pacman_character "c"
@spacing_character " "
@progress_character "-"
@meal_character "o"
@meal_spacing 2
@spec render(number, number, keyword) :: nil | :ok
@doc """
Writes the progress bar to the terminal using IO.write/2. Calling this function again will overwrite the last progress bar. If all tasks are completed it will automatically print a newline character
Use PacmanProgressBar.raw/2 to get the progress bar in the form of a string
## Options
- `:use_color` (boolean) - set to true if you want the pacman character to be yellow
- `:bar_size` (integer) - the amount of characters between the square brackets. A correct value can be calculated by changing `n` in `2 + (n * 3)`
## Examples
iex> PacmanProgressBar.render(30, 7)
#=> [------c o o o o o o o ] 23%
iex> PacmanProgressBar.render(30, 7)
PacmanProgressBar.render(30, 10)
#=> [---------C o o o o o o ] 33%
"""
def render(total_tasks, completed_tasks, opts \\ [])
def render(total_tasks, completed_tasks, opts) do
IO.write("\r#{raw(total_tasks, completed_tasks, opts)}")
if total_tasks == completed_tasks do
IO.write("\n")
end
end
@spec raw(number, number, keyword) :: nonempty_binary
@doc """
Returns the progress bar in the form of a string
Use PacmanProgressBar.render/2 if you automatically want to write this string to the terminal
## Options
- `:use_color` (boolean) - set to true if you want the pacman character to be yellow
- `:bar_size` (integer) - the amount of characters between the square brackets. A correct value can be calculated by changing `n` in `2 + (n * 3)`
## Examples
iex> PacmanProgressBar.raw(30, 7)
#=> "[------c o o o o o o o ] 23%"
"""
def raw(total_tasks, completed_tasks, opts \\ [])
def raw(total_tasks, completed_tasks, _opts) when completed_tasks > total_tasks,
do: raise("completed_tasks cannot be higher than total_tasks")
def raw(total_tasks, completed_tasks, opts) do
bar_size = Keyword.get(opts, :bar_size)
use_color? = Keyword.get(opts, :use_color)
bar_size =
if bar_size == nil or not is_number(bar_size) do
determine_terminal_width() |> determine_bar_size()
else
bar_size
end
bar = initial_bar(bar_size)
percentage_completed = percentage_completed(total_tasks, completed_tasks)
destination_index = percentage_to_index(bar_size, percentage_completed)
bar = move_pacman_to_index(bar, destination_index, 0, @initial_pacman_character, use_color?)
"#{bar |> add_borders_to_bar()} #{percentage_completed |> format_percentage()}"
end
defp add_borders_to_bar(bar), do: "[#{bar}]"
defp format_percentage(percentage) do
cond do
percentage < 10 -> " #{percentage}%"
percentage < 100 -> " #{percentage}%"
percentage == 100 -> "#{percentage}%"
end
end
defp percentage_completed(total_tasks, completed_tasks) do
(completed_tasks / total_tasks)
|> Float.round(2)
|> Kernel.*(100)
|> trunc()
end
# initial_bar creates a progress bar where a meal is placed every x characters
# where x is @meal_spacing
defp initial_bar(bar_size, bar \\ "", counter \\ 0, mealcounter \\ 0)
defp initial_bar(bar_size, bar, counter, _) when counter == bar_size,
do: bar |> String.to_charlist()
defp initial_bar(bar_size, bar, counter, mealcounter) when mealcounter == @meal_spacing do
bar = bar <> @meal_character
initial_bar(bar_size, bar, counter + 1)
end
defp initial_bar(bar_size, bar, counter, mealcounter) when counter < bar_size do
bar = bar <> @spacing_character
initial_bar(bar_size, bar, counter + 1, mealcounter + 1)
end
defp percentage_to_index(bar_size, percentage_completed),
do: (percentage_completed / 100 * bar_size) |> Float.round() |> trunc()
defp move_pacman_to_index(
bar,
destination_index,
current_index,
_pacman_character,
_use_color?
)
when current_index > destination_index,
do: bar
defp move_pacman_to_index(
bar,
destination_index,
current_index,
pacman_character,
use_color?
)
when current_index == 0 do
bar =
if use_color? == true do
List.replace_at(bar, 0, pacman_character |> colorize_pacman_character)
else
List.replace_at(bar, 0, pacman_character)
end
pacman_character = alternate_pacman_character(pacman_character)
move_pacman_to_index(
bar,
destination_index,
current_index + 1,
pacman_character,
use_color?
)
end
defp move_pacman_to_index(
bar,
destination_index,
current_index,
pacman_character,
use_color?
)
when current_index <= destination_index do
bar =
if use_color? == true do
bar = List.replace_at(bar, current_index, pacman_character |> colorize_pacman_character())
List.replace_at(bar, current_index - 1, @progress_character)
else
bar = List.replace_at(bar, current_index, pacman_character)
List.replace_at(bar, current_index - 1, @progress_character)
end
pacman_character =
if rem(current_index, 2) == 0 do
alternate_pacman_character(pacman_character)
else
pacman_character
end
move_pacman_to_index(
bar,
destination_index,
current_index + 1,
pacman_character,
use_color?
)
end
defp colorize_pacman_character(pacman_character) do
IO.ANSI.yellow() <> pacman_character <> IO.ANSI.reset()
end
defp alternate_pacman_character(pacman_character) do
case pacman_character do
"c" -> "C"
"C" -> "c"
end
end
@fallback_width 80
defp determine_terminal_width do
case :io.columns() do
{:ok, count} -> count
_ -> @fallback_width
end
end
defp determine_bar_size(terminal_width) do
# (2 + (n * 3))
# (2 + (32 * 3)) = 98, the maximum
Enum.reduce_while(32..1, 0, fn n, bar_size ->
possible_bar_size = 2 + n * 3
if terminal_width - 7 >= possible_bar_size do
{:halt, bar_size + possible_bar_size}
else
{:cont, bar_size}
end
end)
end
end