DefaultSpinners.java

package net.morimekta.terminal.progress;

import net.morimekta.strings.chr.Char;
import net.morimekta.strings.chr.Color;
import net.morimekta.strings.chr.Unicode;

import java.time.Duration;
import java.util.List;

import static net.morimekta.collect.UnmodifiableList.asList;
import static net.morimekta.strings.StringUtil.clipWidth;
import static net.morimekta.strings.StringUtil.printableWidth;
import static net.morimekta.strings.StringUtil.rightPad;

/**
 * Which spinner to show. Some may require extended unicode font to be used in the console without just showing '?'.
 */
public enum DefaultSpinners implements Spinner {
    /**
     * Simple ASCII spinner using '|', '/', '-', '\'. This variant will work in any terminal.
     */
    ASCII(new Unicode('#'),
          new Unicode('-'),
          new Unicode('v'),
          new Unicode[]{
                  new Unicode('|'),
                  new Unicode('/'),
                  new Unicode('-'),
                  new Unicode('\\')
          }),

    /**
     * Using a block char that bounces up and down to show progress. Not exactly <i>spinning</i>, but does the job.
     * Using unicode chars 0x2581 -&gt; 0x2588;
     * <p>
     * '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'
     */
    BLOCKS(new Unicode('▓'),
           new Unicode('⋅'),
           new Unicode('✓'),
           new Unicode[]{
                   new Unicode('▁'),  // 1/8 block
                   new Unicode('▂'),  // 2/8 block
                   new Unicode('▃'),  // ...
                   new Unicode('▄'),  //
                   new Unicode('▅'),  //
                   new Unicode('▆'),  // ...
                   new Unicode('▇'),  // 7/8 block
                   new Unicode('█'),  // 8/8 (full) block
                   new Unicode('▇'),  // 7/8 block
                   new Unicode('▆'),  // ...
                   new Unicode('▅'),  //
                   new Unicode('▄'),  //
                   new Unicode('▂'),  // ...
                   new Unicode('▁'),  // 2/8 block
           }),
    ;

    @Override
    public String atProgress(double fraction, int spinnerPos, Duration remaining, int width) {
        var progressWidth = width - offset;
        var doneWidth = (int) Math.round(progressWidth * Math.min(1.0, fraction));
        var remainingWidth = progressWidth - doneWidth;
        var percent = (int) Math.round(fraction * 100.0);
        return String.format("[%s%s%s%s%s] %3d%% %s%s%s%s",
                             Color.GREEN,
                             done.toString().repeat(doneWidth),
                             Color.YELLOW,
                             remain.toString().repeat(remainingWidth),
                             Color.CLEAR,
                             percent,
                             new Color(Color.YELLOW, Color.BOLD),
                             spinner.get(spinnerPos % spinner.size()),
                             Color.CLEAR,
                             remaining == null
                             ? "            "
                             : " + " + formatDuration(remaining));
    }

    @Override
    public String atComplete(Duration spent, int width) {
        var progressWidth = width - offset;
        return String.format("[%s%s%s] 100%% %s%s%s @ %s",
                             Color.GREEN,
                             done.toString().repeat(progressWidth),
                             Color.CLEAR,
                             new Color(Color.GREEN, Color.BOLD),
                             complete,
                             Color.CLEAR,
                             formatDuration(spent));
    }

    @Override
    public String atStopped(double fraction, String message, int width) {
        var progressWidth = width - offset;
        var doneWidth = (int) Math.round(progressWidth * Math.min(1.0, fraction));
        var remainingWidth = progressWidth - doneWidth;
        var percent = (int) Math.round(fraction * 100.0);
        return String.format("[%s%s%s%s%s] %3d%% %s%s%s",
                             Color.GREEN,
                             done.toString().repeat(doneWidth),
                             Color.YELLOW,
                             remain.toString().repeat(remainingWidth),
                             Color.CLEAR,
                             percent,
                             new Color(Color.RED, Color.BOLD),
                             rightPad(clipWidth(message, 13), 13),
                             Color.CLEAR);
    }

    private final Char       done;
    private final Char       remain;
    private final Char       complete;
    private final List<Char> spinner;
    private final int        offset;

    DefaultSpinners(Char done,
                    Char remain,
                    Char complete,
                    Char[] spinner) {
        this.done = done;
        this.remain = remain;
        this.complete = complete;
        this.spinner = asList(spinner);
        this.offset = 20 + printableWidth(spinner[0].toString());
    }

    private static String formatDuration(Duration duration) {
        long h = duration.toHours();
        long m = duration.minusHours(h).toMinutes();
        if (h > 0) {
            return String.format("%2d:%02d Hrs", h, m);
        }
        long s = duration.minusHours(h).minusMinutes(m).getSeconds();
        if (m > 0) {
            return String.format("%2d:%02d min", m, s);
        }
        long ms = duration.minusHours(h).minusMinutes(m).minusSeconds(s).toMillis();
        return String.format("   %2d.%1d s", s, ms / 100);
    }
}