Skip to content

Commit 279dcdb

Browse files
feat: add PLAIN variant progress bar
1 parent fab1f43 commit 279dcdb

File tree

9 files changed

+198
-15
lines changed

9 files changed

+198
-15
lines changed

.changeset/clean-ladybugs-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@opentf/cli-pbar': minor
3+
---
4+
5+
Feature: Added `PLAIN` variant & fixed no progress bars on CI env.

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ multiPBar.stop();
9595
```
9696

9797
> [!TIP]
98-
> It is recommended to use the `MEDIUM` sized bars in multi progress bars for better visuals.
98+
> It is recommended to use the `MEDIUM` sized bars in multi progress bars to get better visuals.
9999
100100
## Examples
101101

@@ -127,6 +127,52 @@ pBar.stop();
127127

128128
![Count Demo](./assets/count-demo.png)
129129

130+
---
131+
132+
Rendering a plain variant progress bar.
133+
134+
```js
135+
import { ProgressBar } from '@opentf/cli-pbar';
136+
137+
const pBar = new ProgressBar({
138+
variant: 'PLAIN',
139+
prefix: 'Downloading',
140+
});
141+
142+
pBar.start({ total: 3 });
143+
pBar.inc();
144+
pBar.stop();
145+
```
146+
147+
![Plain Variant Demo](./assets/plain-demo.png)
148+
149+
---
150+
151+
It does not render progress bars in non TTY terminals, like CI, etc.
152+
153+
```js
154+
import { sleep, aForEach } from '@opentf/std';
155+
import { ProgressBar } from '@opentf/cli-pbar';
156+
157+
const arr = ['File 1', 'File 2', 'File 3'];
158+
const pBar = new ProgressBar({
159+
prefix: 'Downloading',
160+
showPercent: false,
161+
showCount: true,
162+
});
163+
164+
pBar.start({ total: arr.length });
165+
166+
await aForEach(arr, async (f) => {
167+
pBar.inc({ suffix: f });
168+
await sleep(500);
169+
});
170+
171+
pBar.stop();
172+
```
173+
174+
![CI Demo](./assets/ci-demo.png)
175+
130176
## API
131177

132178
### options:
@@ -143,6 +189,7 @@ pBar.stop();
143189
| autoClear | boolean | false | If true, then it auto-clears the progress bar after the `stop` method is called. |
144190
| showPercent | boolean | true | If false, then it hides the progress bar percent. |
145191
| showCount | boolean | false | If true, then it show the progress bar count. |
192+
| variant | string | 'STANDARD' | There are two variants available, `STANDARD` & `PLAIN`. |
146193

147194
### Instance methods:
148195

__tests__/progressBar.spec.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { ProgressBar } from '../src/index.ts';
44
import {
55
DEFAULT_BAR_CHAR,
66
MEDIUM_BAR_CHAR,
7+
PLAIN_DONE_BAR_CHAR,
8+
PLAIN_NOT_DONE_BAR_CHAR,
79
SMALL_BAR_CHAR,
810
} from '../src/constants.ts';
911

@@ -47,6 +49,14 @@ function getBarsStr(n, done = false) {
4749
).repeat(n);
4850
}
4951

52+
function getPlainBarsStr(n, done = false, color = '') {
53+
return style(
54+
`$${color}.${done ? 'bol' : 'dim'}{${
55+
done ? PLAIN_DONE_BAR_CHAR : PLAIN_NOT_DONE_BAR_CHAR
56+
}}`
57+
).repeat(n);
58+
}
59+
5060
function getBars(complete = 0, percent = 0, opt = {}) {
5161
const options = {
5262
width: 30,
@@ -210,4 +220,53 @@ describe('Single Progress Bar', () => {
210220
const outBars = getBarsStr(1, true) + getBarsStr(2) + ' [2/3]';
211221
expect(output[2]).toStrictEqual(outBars);
212222
});
223+
224+
it('renders plain progress bar', async () => {
225+
const output = await run(
226+
async (pBar) => {
227+
pBar.start({ total: 3 });
228+
pBar.inc();
229+
pBar.inc();
230+
pBar.stop();
231+
},
232+
{ width: 3, variant: 'PLAIN' }
233+
);
234+
const outBars =
235+
'[' + getPlainBarsStr(1, true) + getPlainBarsStr(2) + ']' + ' 66%';
236+
expect(output[2]).toStrictEqual(outBars);
237+
});
238+
239+
it('renders plain progress bar with colors', async () => {
240+
const output = await run(
241+
async (pBar) => {
242+
pBar.start({ total: 3 });
243+
pBar.inc();
244+
pBar.inc();
245+
pBar.stop();
246+
},
247+
{ width: 3, variant: 'PLAIN', color: 'g', bgColor: 'r' }
248+
);
249+
const outBars =
250+
'[' +
251+
getPlainBarsStr(1, true, 'g') +
252+
getPlainBarsStr(2, false, 'r') +
253+
']' +
254+
' 66%';
255+
expect(output[2]).toStrictEqual(outBars);
256+
});
257+
258+
it('renders no progress bars when the stream is not TTY', async () => {
259+
const output = await run(
260+
async (pBar) => {
261+
pBar.start({ total: 3 });
262+
pBar.inc();
263+
pBar.inc();
264+
pBar.inc();
265+
pBar.stop();
266+
},
267+
{ width: 3, prefix: 'Downloading' },
268+
false
269+
);
270+
expect(output[3]).toBe('⏳ Downloading 100%\n');
271+
});
213272
});

assets/ci-demo.png

4.72 KB
Loading

assets/plain-demo.png

2.7 KB
Loading

demo.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import https from 'node:https';
22
import path from 'node:path';
3-
import { sleep } from '@opentf/std';
3+
import { aForEach, sleep } from '@opentf/std';
44
import { style } from '@opentf/cli-styles';
55
import { ProgressBar } from './src';
66

@@ -222,10 +222,30 @@ async function styledTexts() {
222222
multiPBar.stop();
223223
}
224224

225+
async function plain() {
226+
const arr = ['Apple', 'Mango', 'Orange', 'Grapes', 'Pear', 'Guava'];
227+
228+
const pBar = template('Plain', {
229+
variant: 'PLAIN',
230+
prefix: 'Downloading',
231+
showPercent: true,
232+
showCount: false,
233+
});
234+
pBar.start({ total: arr.length });
235+
236+
await aForEach(arr, async (f) => {
237+
await sleep(500);
238+
pBar.inc({ suffix: f });
239+
});
240+
241+
pBar.stop();
242+
}
243+
225244
await defaultBar();
226245
await mediumBar();
227246
await smallBar();
228247
await prefixSuffix();
229248
// await downloading();
230249
await autoClear();
231250
await styledTexts();
251+
await plain();

src/ProgressBar.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import {
99
shallowMerge,
1010
} from '@opentf/std';
1111
import { type Bar, type BarSize, type Options } from './types';
12-
import { DEFAULT_BAR_CHAR, MEDIUM_BAR_CHAR, SMALL_BAR_CHAR } from './constants';
12+
import {
13+
DEFAULT_BAR_CHAR,
14+
MEDIUM_BAR_CHAR,
15+
PLAIN_DONE_BAR_CHAR,
16+
PLAIN_NOT_DONE_BAR_CHAR,
17+
SMALL_BAR_CHAR,
18+
} from './constants';
1319

1420
class ProgressBar {
1521
private _options: Options = {
@@ -23,11 +29,16 @@ class ProgressBar {
2329
suffix: '',
2430
showPercent: true,
2531
showCount: false,
32+
variant: 'STANDARD',
2633
};
2734
private _bars: Bar[];
2835

2936
constructor(options?: Partial<Options>) {
30-
this._options = shallowMerge(this._options, options as object) as Options;
37+
const opts =
38+
options?.variant === 'PLAIN'
39+
? { ...this._options, color: '', bgColor: '' }
40+
: this._options;
41+
this._options = shallowMerge(opts, options as object) as Options;
3142
this._bars = [];
3243
}
3344

@@ -43,22 +54,34 @@ class ProgressBar {
4354
}
4455

4556
private _getBars(bar: Bar, percent: number): string {
46-
const barChar = this._getBarCharBySize(bar.size);
57+
let doneBars, bgBars;
4758
const color = bar.color || this._options.color;
4859
const bgColor = bar.bgColor || this._options.bgColor;
4960
const percentVal = Math.trunc(percentageOf(percent, this._options.width));
50-
const doneBars = style(`$${color}.bol{${barChar}}`).repeat(percentVal);
51-
const bgBars = style(`$${bgColor}.dim{${barChar}}`).repeat(
52-
this._options.width - percentVal
53-
);
61+
62+
if (this._options.variant === 'PLAIN') {
63+
doneBars = style(`$${color}.bol{${PLAIN_DONE_BAR_CHAR}}`).repeat(
64+
percentVal
65+
);
66+
bgBars = style(`$${bgColor}.dim{${PLAIN_NOT_DONE_BAR_CHAR}}`).repeat(
67+
this._options.width - percentVal
68+
);
69+
} else {
70+
const barChar = this._getBarCharBySize(bar.size);
71+
doneBars = style(`$${color}.bol{${barChar}}`).repeat(percentVal);
72+
bgBars = style(`$${bgColor}.dim{${barChar}}`).repeat(
73+
this._options.width - percentVal
74+
);
75+
}
76+
5477
return doneBars + bgBars;
5578
}
5679

5780
private _render() {
5881
this._bars.forEach((b, i) => {
5982
let str = '';
6083

61-
if (i > 0) {
84+
if (i > 0 && this._options.stream.isTTY) {
6285
this._options.stream.write(EOL);
6386
}
6487

@@ -74,17 +97,21 @@ class ProgressBar {
7497
const showCount = Object.hasOwn(b, 'showCount')
7598
? b.showCount
7699
: this._options.showCount;
100+
const variant = Object.hasOwn(b, 'variant')
101+
? b.variant
102+
: this._options.variant;
77103

78-
if (b.progress) {
79-
const percent = b.total
80-
? Math.trunc(percentage(isNaN(b.value) ? 0 : b.value, b.total))
81-
: 0;
104+
const percent = b.total
105+
? Math.trunc(percentage(isNaN(b.value) ? 0 : b.value, b.total))
106+
: 0;
107+
108+
if (b.progress && this._options.stream.isTTY) {
82109
const bar = this._getBars(b, percent);
83110
str += (
84111
intersperse(
85112
compact([
86113
prefix,
87-
bar,
114+
variant === 'PLAIN' ? `[${bar}]` : bar,
88115
showPercent ? percent + '%' : null,
89116
showCount ? `[${b.value || 0}/${b.total || 0}]` : null,
90117
suffix,
@@ -96,6 +123,23 @@ class ProgressBar {
96123
str += prefix + ' ' + suffix;
97124
}
98125

126+
if (!this._options.stream.isTTY) {
127+
str = (
128+
intersperse(
129+
compact([
130+
'⏳',
131+
prefix,
132+
showPercent ? percent + '%' : null,
133+
showCount ? `[${b.value || 0}/${b.total || 0}]` : null,
134+
suffix,
135+
]),
136+
' '
137+
) as string[]
138+
).join('');
139+
this._options.stream.write(str + EOL);
140+
return;
141+
}
142+
99143
if (
100144
this._options.stream.cursorTo(0) &&
101145
this._options.stream.clearLine(0)

src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
export const DEFAULT_BAR_CHAR = '\u{2588}';
2+
23
export const SMALL_BAR_CHAR = '\u{2501}';
4+
35
export const MEDIUM_BAR_CHAR = '\u{2586}';
6+
7+
export const PLAIN_DONE_BAR_CHAR = '=';
8+
9+
export const PLAIN_NOT_DONE_BAR_CHAR = '-';

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export type Options = {
2323
showPercent: boolean;
2424
/** Show hide progress bar count */
2525
showCount: boolean;
26+
/** Renders the progress bars based on the variant */
27+
variant: 'PLAIN' | 'STANDARD';
2628
};
2729

2830
export type Bar = Omit<Options, 'stream' | 'width' | 'autoClear'> & {

0 commit comments

Comments
 (0)