UNPKG

symfony-style-console

Version:

Use the style and utilities of the Symfony Console in Node.js

529 lines (528 loc) 16.3 kB
import { VERBOSITY_QUIET, VERBOSITY_VERBOSE, VERBOSITY_VERY_VERBOSE, VERBOSITY_DEBUG } from '../Output/OutputInterface'; import { countOccurences, lengthWithoutDecoration, formatTime, formatMemory, strPad, sprintf, time } from '../Helper/Helper'; import ConsoleOutput from '../Output/ConsoleOutput'; /** * The ProgressBar provides helpers to display progress output. * * @author Fabien Potencier <fabien@symfony.com> * * Original PHP class * * @author Chris Jones <leeked@gmail.com> * * Original PHP class * * @author Florian Reuschel <florian@loilo.de> * * Port to TypeScript * */ export default class ProgressBar { /** * Creates a new progress bar. * * @param OutputInterface output An OutputInterface instance * @param int max Maximum steps (0 if unknown) */ constructor(output, max = 0) { /** * The width of the progress bar. */ this.barWidth = 28; /** * The character that represents uncompleted progress. */ this.emptyBarChar = '-'; /** * The character that represents the outer pointer of the completed progress. */ this.progressChar = '>'; /** * The frequency of redrawing the bar (in steps). */ this.redrawFreq = 1; /** * The current step of the progress. */ this.step = 0; /** * The current progress in percent. */ this.percent = 0.0; /** * A Hash containing mapping format template placeholders to custom messages. */ this.messages = {}; /** * If the progress should be rewritten to the same position. */ this.shouldOverwrite = true; /** * Indicator for the `overwrite` method if it's the first render cycle. */ this.firstRun = true; if (output instanceof ConsoleOutput) { output = output.getErrorOutput(); } this.output = output; this.setMaxSteps(max); if (!this.output.isDecorated()) { // disable overwrite when output does not support ANSI codes. this.shouldOverwrite = false; // set a reasonable redraw frequency so output isn't flooded this.setRedrawFrequency(max / 10); } this.startTime = time(); } /** * Sets a format for a given name. * * This method also allow you to override an existing format. * * @param name The format name * @param format A format string */ static setFormatDefinition(name, format) { if (!this.formats) { this.formats = this.initFormats(); } this.formats[name] = format; } /** * Gets the format for a given name. * * @param name The format name * @return A format string */ static getFormatDefinition(name) { if (!this.formats) { this.formats = this.initFormats(); } return this.formats[name] || null; } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. * * @param name The placeholder name (including the delimiter char like %) * @param callback A formatter callback */ static setPlaceholderFormatterDefinition(name, callable) { if (!this.formatters) { this.formatters = this.initPlaceholderFormatters(); } this.formatters[name] = callable; } /** * Gets the placeholder formatter for a given name. * * @param name The placeholder name (including the delimiter char like %) * @return A formatter callback */ static getPlaceholderFormatterDefinition(name) { if (!this.formatters) { this.formatters = this.initPlaceholderFormatters(); } return this.formatters[name] || null; } /** * Gets the initially available format templates. */ static initFormats() { return { normal: ' %current%/%max% [%bar%] %percent:3s%%', normal_nomax: ' %current% [%bar%]', verbose: ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', verbose_nomax: ' %current% [%bar%] %elapsed:6s%', very_verbose: ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', very_verbose_nomax: ' %current% [%bar%] %elapsed:6s%', debug: ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', debug_nomax: ' %current% [%bar%] %elapsed:6s% %memory:6s%' }; } /** * Gets the initially available format placeholder callbacks. */ static initPlaceholderFormatters() { return { bar(bar, output) { const completeBars = Math.floor(bar.getMaxSteps() > 0 ? bar.getProgressPercent() * bar.getBarWidth() : bar.getProgress() % bar.getBarWidth()); let display = bar.getBarCharacter().repeat(completeBars); if (completeBars < bar.getBarWidth()) { const emptyBars = bar.getBarWidth() - completeBars - lengthWithoutDecoration(output.getFormatter(), bar.getProgressCharacter()); display += bar.getProgressCharacter() + bar.getEmptyBarCharacter().repeat(emptyBars); } return display; }, elapsed(bar) { return formatTime(time() - bar.getStartTime()); }, remaining(bar) { if (!bar.getMaxSteps()) { throw new Error('Unable to display the remaining time if the maximum number of steps is not set.'); } let remaining; if (!bar.getProgress()) { remaining = 0; } else { remaining = Math.round(((time() - bar.getStartTime()) / bar.getProgress()) * (bar.getMaxSteps() - bar.getProgress())); } return formatTime(remaining); }, estimated(bar) { if (!bar.getMaxSteps()) { throw new Error('Unable to display the estimated time if the maximum number of steps is not set.'); } let estimated; if (!bar.getProgress()) { estimated = 0; } else { estimated = Math.round(((time() - bar.getStartTime()) / bar.getProgress()) * bar.getMaxSteps()); } return formatTime(estimated); }, memory(bar) { return formatMemory(process.memoryUsage().heapTotal); }, current(bar) { return strPad(String(bar.getProgress()), bar.getStepWidth(), ' ', 'STR_PAD_LEFT'); }, max(bar) { return String(bar.getMaxSteps()); }, percent(bar) { return String(Math.floor(bar.getProgressPercent() * 100)); } }; } /** * Associates a text with a named placeholder. * * The text is displayed when the progress bar is rendered but only * when the corresponding placeholder is part of the custom format line * (by wrapping the name with %). * * @param message The text to associate with the placeholder * @param name The name of the placeholder */ setMessage(message, name = 'message') { this.messages[name] = message; } /** * Gets the message associated with a certain placeholder. * * @param name A format placeholder */ getMessage(name = 'message') { return this.messages[name]; } /** * Gets the progress bar start time. */ getStartTime() { return this.startTime; } /** * Gets the progress bar maximal steps. */ getMaxSteps() { return this.max; } /** * Gets the current step position. */ getProgress() { return this.step; } /** * Gets the progress bar step width. */ getStepWidth() { return this.stepWidth; } /** * Gets the current progress bar percent. */ getProgressPercent() { return this.percent; } /** * Sets the progress bar width. */ setBarWidth(size) { this.barWidth = Math.max(1, size); } /** * Gets the progress bar width in characters. */ getBarWidth() { return this.barWidth; } /** * Sets the bar character. * * @param char A character */ setBarCharacter(char) { this.barChar = char; } /** * Gets the bar character. */ getBarCharacter() { if (null == this.barChar) { return this.max ? '=' : this.emptyBarChar; } return this.barChar; } /** * Sets the empty bar character. * * @param char A character */ setEmptyBarCharacter(char) { this.emptyBarChar = char; } /** * Gets the empty bar character. */ getEmptyBarCharacter() { return this.emptyBarChar; } /** * Sets the progress bar character. * * @param char A character */ setProgressCharacter(char) { this.progressChar = char; } /** * Gets the progress bar character. */ getProgressCharacter() { return this.progressChar; } /** * Sets the progress bar format. * * @param format The format */ setFormat(format) { this.format = null; this.internalFormat = format; } /** * Sets the redraw frequency. * * @param freq The frequency in steps */ setRedrawFrequency(freq) { this.redrawFreq = Math.max(freq, 1); } /** * Starts the progress output. * * @param max Number of steps to complete the bar (`0` if indeterminate), `null` to leave unchanged */ start(max = null) { this.startTime = time(); this.step = 0; this.percent = 0.0; if (null != max) { this.setMaxSteps(max); } this.display(); } /** * Advances the progress output X steps. * * @param step Number of steps to advance */ advance(step = 1) { this.setProgress(this.step + step); } /** * Sets whether to overwrite the progress bar, `false` for new line. * * @param overwrite Whether the progress bar should be overwritten */ setOverwrite(overwrite) { this.shouldOverwrite = overwrite; } /** * Sets the current progress. * * @param step The current progress */ setProgress(step) { if (this.max && step > this.max) { this.max = step; } else if (step < 0) { step = 0; } const prevPeriod = Math.round(this.step / this.redrawFreq); const currPeriod = Math.round(step / this.redrawFreq); this.step = step; this.percent = this.max ? this.step / this.max : 0; if (prevPeriod !== currPeriod || this.max === step) { this.display(); } } /** * Finishes the progress output. */ finish() { if (!this.max) { this.max = this.step; } if (this.step === this.max && !this.shouldOverwrite) { // prevent double 100% output return; } this.setProgress(this.max); } /** * Outputs the current progress string. */ display() { if (VERBOSITY_QUIET === this.output.getVerbosity()) { return; } if (null == this.format) { this.setRealFormat(this.internalFormat || this.determineBestFormat()); } this.overwrite(this.buildLine()); } /** * Removes the progress bar from the current line. * * This is useful if you wish to write some output while a progress bar is running. * Call display() to show the progress bar again. */ clear() { if (!this.shouldOverwrite) { return; } if (null == this.format) { this.setRealFormat(this.internalFormat || this.determineBestFormat()); } this.overwrite(''); } /** * Sets the progress bar format template. * * @param format The format template */ setRealFormat(format) { // try to use the _nomax variant if available if (!this.max && null != ProgressBar.getFormatDefinition(format + '_nomax')) { this.format = ProgressBar.getFormatDefinition(format + '_nomax'); } else if (null != ProgressBar.getFormatDefinition(format)) { this.format = ProgressBar.getFormatDefinition(format); } else { this.format = format; } this.formatLineCount = countOccurences(this.format, '\n') || 0; } /** * Sets the progress bar maximal steps. * * @param max The progress bar max steps */ setMaxSteps(max) { this.max = Math.max(0, max); this.stepWidth = this.max ? String(this.max).length : 4; } /** * Overwrites a previous message to the output. * * @param message The message */ overwrite(message) { if (this.shouldOverwrite) { if (!this.firstRun) { // Move the cursor to the beginning of the line this.output.write('\x0D'); // Erase the line this.output.write('\x1B[2K'); // Erase previous lines if (this.formatLineCount > 0) { this.output.write('\x1B[1A\x1B[2K'.repeat(this.formatLineCount)); } } } else if (this.step > 0) { this.output.writeln(''); } this.firstRun = false; this.output.write(message); } /** * Determines the fitting format template for the currently set verbosity. */ determineBestFormat() { switch (this.output.getVerbosity()) { // VERBOSITY_QUIET: display is disabled anyway case VERBOSITY_VERBOSE: return this.max ? 'verbose' : 'verbose_nomax'; case VERBOSITY_VERY_VERBOSE: return this.max ? 'very_verbose' : 'very_verbose_nomax'; case VERBOSITY_DEBUG: return this.max ? 'debug' : 'debug_nomax'; default: return this.max ? 'normal' : 'normal_nomax'; } } /** * Renders the current state of the progress bar. * * @return The output of the progress bar's current state */ buildLine() { const regex = /%([a-z\-_]+)(||\:([^%]+))?%/gi; const callback = (...args) => { const str = args.pop(); const offset = args.pop(); const matches = args; const formatter = ProgressBar.getPlaceholderFormatterDefinition(matches[1]); let text; if (formatter) { text = formatter(this, this.output); } else if (this.messages[matches[1]]) { text = this.messages[matches[1]]; } else { return matches[0]; } if (matches[3]) { text = sprintf(`%${matches[3]}`, text); } return text; }; const line = this.format.replace(regex, callback); const lineLength = lengthWithoutDecoration(this.output.getFormatter(), line); const terminalWidth = process.stdout.columns || 40; if (lineLength <= terminalWidth) { return line; } this.setBarWidth(this.barWidth - lineLength + terminalWidth); return this.format.replace(regex, callback); } }