henrik / progress_bar

Command-line progress bars and spinners for Elixir.
MIT License
329 stars 20 forks source link

Multiple bars #27

Closed fuelen closed 2 years ago

fuelen commented 2 years ago

This library doesn't support multiple progress bars and this is the reason why I wrote custom solution which I'd like to share here.

Implementation:

defmodule ProgBar do
  use GenServer

  def start(max_values_with_labels) do
    GenServer.start_link(__MODULE__, max_values_with_labels, name: __MODULE__)
  end

  def inc(label, size \\ 1) do
    GenServer.cast(__MODULE__, {:inc, label, size})
  end

  @bar_width 50
  @label_right_padding 1
  def render(state, init? \\ false) do
    lines = map_size(state)

    max_label_length =
      state
      |> Map.keys()
      |> Enum.map(fn atom -> atom |> to_string |> String.length() end)
      |> Enum.max()
      |> Kernel.+(@label_right_padding)

    init_acc = if init?, do: String.duplicate("\n", lines), else: ""
    init_acc = init_acc <> IO.ANSI.cursor_up(lines)

    state
    |> Enum.reduce(init_acc, fn {label, %{current: current, max: max}}, acc ->
      filled = if(max > 0, do: trunc(current / max * @bar_width), else: 0)

      [
        acc,
        String.pad_trailing(to_string(label), max_label_length),
        "[",
        String.duplicate("=", filled),
        String.duplicate(" ", max(@bar_width - filled, 0)),
        "] ",
        to_string(current),
        "/",
        to_string(max),
        "\n"
      ]
    end)
    |> IO.write()
  end

  @impl true
  def init(max_values_with_labels) do
    state =
      Enum.into(max_values_with_labels, %{}, fn {label, max} ->
        {label, %{current: 0, max: max}}
      end)

    send(self(), {:render, true})
    {:ok, state}
  end

  @impl true
  def handle_cast({:inc, label, size}, state) do
    new_state = update_in(state, [label, :current], &(&1 + size))
    {:noreply, new_state}
  end

  @impl true
  def handle_info({:render, init?}, state) do
    render(state, init?)
    Process.send_after(self(), {:render, false}, 300)
    {:noreply, state}
  end
end

Usage example:


estimate = %{
  "Users" => 300,
  "Posts" => 400,
  "Categories" => 10,
  "Downloads" => 150
}

ProgBar.start(estimate)

estimate
|> Enum.map(fn {label, max_number} ->
  Task.async(fn ->
    1..max_number
    |> Enum.each(fn _ ->
      Process.sleep(Enum.random([50, 200, 300, 400]))
      ProgBar.inc(label)
    end)
  end)
end)
|> Task.yield_many(:infinity)
henrik commented 2 years ago

Thank you, @fuelen! (And apologies for not replying sooner.) If you don't mind, I'll close this ticket since it doesn't represent an open issue (right?), but it will be in the issues list for others to find :)

fuelen commented 2 years ago

@henrik yes, this is exactly a purpose of the issue, just for history :)