bardot
Version:
Yet another console progress bar
104 lines (82 loc) • 2.95 kB
Flow
// @flow
import len from 'string-length'; // ignores ansi escape sequences
import type { Option } from './build';
type OptData = {
opt: Option,
data: {
ctCharBar: number,
ctBlockPerNum: number,
ctPipPerNum: number,
},
};
type ReplaceMap = {
[key: string]: string,
};
const replace = (tpl: string, map: ReplaceMap): string => tpl.replace(
/\|(bar|cur|max|pct)\|/g,
(match: string, word: string): string => map[word],
);
const ctCharLabelMax = (opt: Option): number => len(replace(opt.tpl, {
cur: opt.max.toString(),
max: opt.max.toString(),
pct: '99.9', // more chars than '100'
bar: '',
}));
const ctCharBarDerive = (width, opt) => {
switch (width.mode) {
// simplest case: the size of the bar is exactly as configured
case 'bar': {
return width.ctChar;
}
// user configures the total size of bar + label.
// derive the bar size by subtracting the maximum label size.
case 'template': {
return width.ctChar - ctCharLabelMax(opt);
}
// user wants to fill the available width, minus the configured number of characters.
case 'fill': {
// $FlowFixMe flow moans at process.stdout.columns, no idea how to fix
const ctCharFull = width.ctCharFull || process.stdout.columns;
return ctCharFull - width.ctCharMinus - ctCharLabelMax(opt);
}
default: {
return 0;
}
}
};
const addData = (opt: Option): OptData => {
const ctCharBar = ctCharBarDerive(opt.width, opt);
return {
opt,
data: {
ctCharBar,
ctBlockPerNum: ctCharBar / opt.max,
ctPipPerNum: (ctCharBar * (opt.symbol.fractions.length + 1)) / opt.max,
},
};
};
const bar = ({ opt, data }) => {
const ctBlock = opt.cur * data.ctBlockPerNum;
const sBlocks = opt.symbol.full.repeat(ctBlock);
const sEmpties = opt.symbol.empty.repeat((opt.max - opt.cur) * data.ctBlockPerNum);
if (sBlocks.length + sEmpties.length === data.ctCharBar) {
return sBlocks + sEmpties;
}
const arsPart = [opt.symbol.empty].concat(opt.symbol.fractions || []);
const sPart = arsPart[Math.floor(opt.cur * data.ctPipPerNum) % arsPart.length];
return sBlocks + sPart + sEmpties;
};
const renderOptData = (od) => {
if (od.data.ctCharBar < 0) {
return ''; // no room: return empty string rather than junk
}
const map = {
// right-align cur and pct within their space to avoid overall length changes
cur: od.opt.cur.toString().padStart(od.opt.max.toString().length),
max: od.opt.max.toString(),
pct: (Math.round((1000 * od.opt.cur) / od.opt.max) / 10).toString().padStart(4),
bar: bar(od),
};
return replace(od.opt.tpl, map);
};
export default (opt: Option): string => renderOptData(addData(opt));