SamVerschueren / listr

Terminal task list
MIT License
3.29k stars 108 forks source link

Add a progressbar option #86

Open Ryux opened 6 years ago

Ryux commented 6 years ago

Hi,

Is it possible to add a progress bar to see the different part of a process ?

Example: to see the download of a file

Thanks

SamVerschueren commented 6 years ago

Can you elaborate? Just one progress bar for all the tasks, or as output of a certain task?

kacpak commented 6 years ago

+1 I think that an option to enable progress bar for certain tasks would be great

emilioriosvz commented 6 years ago

+1 I use progressbar but listr gets a little crazy and begins to duplicate tasks (he executes them once, but he shows them many times)

Ryux commented 6 years ago

Hi,

In my case, it would be to have to progress bar for a certain task. Some of my jobs are slow and I need to send a feedback to the user. A progress bar would be perfect.

Thanks

TheDocTrier commented 6 years ago

I think what you're looking for could be accomplished using an observable. This is implemented in TypeScript (I wish the listr definitions were in the main project :wink: ).

Example:

import Listr = require("listr");
import rxjs = require("rxjs");

function createBar(progress: number, length: number): string {
  // Determine distance finished.
  let distance = progress * length;
  // Start progress bar.
  let bar = "[";
  // Add main portion.
  bar += "=".repeat(Math.floor(distance));
  // Add intermediate porttion.
  bar += distance % 1 > 0.5 ? "-" : "";
  // Extend empty bar.
  bar += " ".repeat(length > bar.length ? length - bar.length : 0);
  // Cap progress bar.
  bar += "]";
  return bar;
}

const listr = new Listr([
  {
    title: "Placeholder",
    task: () =>
      new rxjs.Observable(observer => {
        let progress = 0;
        let timer = setInterval(() => {
          // Calculate current progress.
          if (progress < 1) {
            progress += 0.002;
            observer.next(createBar(progress, 40));
          } else {
            clearInterval(timer);
            observer.complete();
          }
        }, 10);
      })
  }
]);
listr.run();

// Approximate Output (s=spinner):
// S Placeholder
//  > [=====-           ]
TheDocTrier commented 6 years ago

If this was to be implemented, should there be several presets? @SamVerschueren @Ryux I'm worried that a progress bar implementation might fit better in a separate package, similar to cli-spinners.

For example, a simple bar with a format string "<endCap><baseFiller><partial1><partial2>,,,<endCap>" and a percentage formatter:

/** Function that converts a number [0,1] to a string representation. */
type ProgressView = (progress: number, ...args: any[]) => string;

const regexBarFormat = /^(.)(.+)(.)$/;

interface BarFormat {
  caps: { left: string; right: string };
  components: string[];
  empty: string; // Defaults to a space.
}

type StrBarFormat = string | BarFormat;

/** Produce an easier to use formatting object. */
function genFormat(format: StrBarFormat): BarFormat {
  // Check if format is a BarFormat through exclusion.
  if (format.constructor !== String) {
    return format as BarFormat;
  }

  // Break up the formatting string.
  let [match, capLeft, componentString, capRight] = (format as string).match(
    regexBarFormat
  ) as Array<string>;

  return {
    caps: { left: capLeft, right: capRight },
    components: componentString.split(""),
    empty: " "
  };
}

/**
 * Standard bar format.
 *
 * Example: [==-  ]
 */
function bar(progress: number, length: number, format: StrBarFormat): string {
  let barFormat = genFormat(format);
  // Account for end caps.
  let innerLength =
    length - (barFormat.caps.left.length + barFormat.caps.right.length);
  let distance = progress * innerLength;
  let base = Math.floor(distance);
  let increment = Math.floor((distance % 1) * barFormat.components.length);

  // Begin the inside.
  let inside = barFormat.components[0].repeat(base);
  // Append the partial increment.
  if (base < distance && increment > 0) {
    inside += barFormat.components[increment];
  }
  // Append missing empty space.
  inside += barFormat.empty.repeat(innerLength - inside.length);

  return barFormat.caps.left + inside + barFormat.caps.right;
}

/**
 * Simple percentage formatter.
 *
 * Example: 37.6%
 */
function percentage(progress: number, precision: number = 1): string {
  return (progress * 100).toFixed(precision) + "%";
}
ghengeveld commented 4 years ago

In case anyone's looking to implement a progress bar without using observables, here's a basic implementation:

const repeat = (n, char) => [...new Array(Math.round(n))].map(() => char);
const progress = (percentage, size = 20) => {
  const track = repeat(size, " ");
  const completed = repeat((percentage / 100) * size, "=");
  return `${completed.join("")}${track.join("")}`.substr(0, size);
};

Then inside your task function somewhere:

const interval = setInterval(() => {
  task.output = `[${progress(ctx.percentage)}] ${ctx.percentage.toFixed(1)}%`;
  if (ctx.percentage >= 100) clearInterval(interval);
}, 200)

This assumes you're keeping track of progress in ctx.percentage. Of course you could do this without an interval, it totally depends on what you're tracking progress of. Usually there's an update handler of sorts, in which case you don't need the interval.

Feel free to use and adapt.