├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── multiProgressBar.spec.js └── progressBar.spec.js ├── assets ├── ci-demo.png ├── count-demo.png ├── demo-min.gif ├── demo.gif ├── demo.png └── plain-demo.png ├── commitlint.config.cjs ├── demo.js ├── demo.yml ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── ProgressBar.ts ├── constants.ts ├── index.ts └── types.ts ├── tsconfig.json ├── tsup.config.js └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { "node": true }, 4 | "extends": ["@opentf/eslint-config-base"], 5 | "rules": { 6 | "no-control-regex": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | env: 13 | HUSKY: 0 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [18.x, 20.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: pnpm/action-setup@v2 27 | with: 28 | version: 8 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: 'pnpm' 34 | - run: pnpm install 35 | - run: pnpm run ci 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | env: 11 | HUSKY: 0 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | id-token: write 19 | contents: write 20 | pull-requests: write 21 | repository-projects: write 22 | steps: 23 | - name: Checkout Repo 24 | uses: actions/checkout@v3 25 | 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v2 28 | with: 29 | version: 8 30 | 31 | - name: Setup Node.js 20.x 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: 20.x 35 | cache: 'pnpm' 36 | 37 | - name: Install Dependencies 38 | run: pnpm install 39 | 40 | - name: Create Release Pull Request or Publish to npm 41 | id: changesets 42 | uses: changesets/action@v1 43 | with: 44 | # This expects you to have a script called publish-packages which does a build for your packages and calls changeset publish 45 | publish: pnpm publish-packages 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Dependency directories 9 | node_modules/ 10 | jspm_packages/ 11 | 12 | # Yarn 13 | .yarn/* 14 | !.yarn/patches 15 | !.yarn/releases 16 | !.yarn/plugins 17 | !.yarn/sdks 18 | !.yarn/versions 19 | .pnp.* 20 | 21 | # Artifacts 22 | dist 23 | build 24 | built 25 | lib 26 | 27 | play.js 28 | test.[tj]s -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged --concurrent false 5 | 6 | # exit 1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | coverage 3 | build 4 | built 5 | dist 6 | lib -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @opentf/cli-pbar 2 | 3 | ## 0.7.2 4 | 5 | ### Patch Changes 6 | 7 | - b845735: Updated readme with js-std articles. 8 | 9 | ## 0.7.1 10 | 11 | ### Patch Changes 12 | 13 | - 3612706: Updated docs. 14 | 15 | ## 0.7.0 16 | 17 | ### Minor Changes 18 | 19 | - 279dcdb: Feature: Added `PLAIN` variant & fixed no progress bars on CI env. 20 | 21 | ## 0.6.0 22 | 23 | ### Minor Changes 24 | 25 | - b6db659: Fixed not incrementing bars in multi progress bars. 26 | 27 | ## 0.5.0 28 | 29 | ### Minor Changes 30 | 31 | - 91c6883: Added inc method to increment the progress bar value. 32 | 33 | ## 0.4.0 34 | 35 | ### Minor Changes 36 | 37 | - 67c771a: Fixed unnecessary spaces in a row if prefix absent. 38 | 39 | ## 0.3.0 40 | 41 | ### Minor Changes 42 | 43 | - f569890: Changed utils lib to std. 44 | 45 | ### Patch Changes 46 | 47 | - fd46f72: Updated readme. 48 | 49 | ## 0.2.2 50 | 51 | ### Patch Changes 52 | 53 | - 388f534: Updated docs with utils lib announcement. 54 | 55 | ## 0.2.1 56 | 57 | ### Patch Changes 58 | 59 | - e06dfac: Updated readme with node repl link 60 | 61 | ## 0.2.0 62 | 63 | ### Minor Changes 64 | 65 | - 0afc0aa: Added pkg provenance support. 66 | 67 | ## 0.1.8 68 | 69 | ### Patch Changes 70 | 71 | - b93ec98: Fixed readme demo gif url 72 | 73 | ## 0.1.7 74 | 75 | ### Patch Changes 76 | 77 | - 8a08a3a: Moved demo gif to assets dir 78 | 79 | ## 0.1.6 80 | 81 | ### Patch Changes 82 | 83 | - 8108301: Updated demo gif 84 | 85 | ## 0.1.5 86 | 87 | ### Patch Changes 88 | 89 | - d52ba4d: Updated pkg keywords 90 | 91 | ## 0.1.4 92 | 93 | ### Patch Changes 94 | 95 | - 960c526: Updated pkg keywords. 96 | 97 | ## 0.1.3 98 | 99 | ### Patch Changes 100 | 101 | - 7bb4ff3: Changed org name 102 | 103 | ## 0.1.2 104 | 105 | ### Patch Changes 106 | 107 | - 2e79b6a: Fixed readme 108 | 109 | ## 0.1.1 110 | 111 | ### Patch Changes 112 | 113 | - e70f3f0: Updated readme with minified demo gif. 114 | 115 | ## 0.1.0 116 | 117 | ### Minor Changes 118 | 119 | - cb8dc57: Added single & multi progress bar with size variants. 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Thanga Ganapathy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |  [OPEN TECH FOUNDATION](https://open-tech-foundation.pages.dev/) 4 | 5 |
6 | 7 | # @opentf/cli-pbar 8 | 9 | [![Build](https://github.com/open-tech-foundation/cli-progress-bar/actions/workflows/build.yml/badge.svg)](https://github.com/open-tech-foundation/cli-progress-bar/actions/workflows/build.yml) 10 | 11 |
12 | 13 |
14 | 15 | ![Demo](./assets/demo-min.gif) 16 | 17 |
18 | 19 | > The Customizable CLI Progress Bars. 20 | 21 |
22 | 23 | **Try it online at [https://node-repl.pages.dev](https://node-repl.pages.dev/)** 24 | 25 |
26 | 27 | --- 28 | 29 | 🚀 [@opentf/std](https://js-std.pages.dev/) - An Extensive JavaScript Standard Library. Please review and give feedback. 30 | 31 | Please read our important articles: 32 | 33 | - [Introducing Our New JavaScript Standard Library](https://ganapathy.hashnode.dev/introducing-our-new-javascript-standard-library) 34 | 35 | - [You Don’t Need JavaScript Native Methods](https://ganapathy.hashnode.dev/you-dont-need-javascript-native-methods) 36 | 37 | --- 38 | 39 | ## Features 40 | 41 | - Single & Multi Progress Bars 42 | 43 | - Customizable (colors, size, etc) 44 | 45 | - TypeScript Support 46 | 47 | ## Installation 48 | 49 | Install it using your favourite package manager. 50 | 51 | ```bash 52 | npm install @opentf/cli-pbar 53 | ``` 54 | 55 | ```bash 56 | yarn add @opentf/cli-pbar 57 | ``` 58 | 59 | ```bash 60 | pnpm add @opentf/cli-pbar 61 | ``` 62 | 63 | ```bash 64 | bun add @opentf/cli-pbar 65 | ``` 66 | 67 | ## Syntax 68 | 69 | ```ts 70 | new ProgressBar(options?: Options) 71 | ``` 72 | 73 | ## Usage 74 | 75 | Single progress bar. 76 | 77 | ```ts 78 | import { ProgressBar } from '@opentf/cli-pbar'; 79 | 80 | const pBar = new ProgressBar(); 81 | pBar.start({ total: 100 }); 82 | pBar.update({ value: 50 }); 83 | pBar.update({ value: 100 }); 84 | pBar.stop(); 85 | ``` 86 | 87 | Multi progress bar. 88 | 89 | ```ts 90 | import { ProgressBar } from '@opentf/cli-pbar'; 91 | 92 | const multiPBar = new ProgressBar({ size: 'MEDIUM' }); 93 | multiPBar.start(); 94 | const b1 = multiPBar.add({ total: 100 }); 95 | const b2 = multiPBar.add({ total: 100 }); 96 | const b3 = multiPBar.add({ total: 100 }); 97 | b1.update({ value: 23 }); 98 | b3.update({ value: 35 }); 99 | b2.update({ value: 17 }); 100 | multiPBar.stop(); 101 | ``` 102 | 103 | > [!TIP] 104 | > It is recommended to use the `MEDIUM` sized bars in multi progress bars to get better visuals. 105 | 106 | ## Examples 107 | 108 | Using `inc()` to increment the progress bar value, hide the `percent` & show the `count`. 109 | 110 | ```js 111 | import { sleep, aForEach } from '@opentf/std'; 112 | import { ProgressBar } from '@opentf/cli-pbar'; 113 | 114 | const arr = ['Apple', 'Mango', 'Orange', 'Grapes', 'Pear', 'Guava']; 115 | 116 | const pBar = new ProgressBar({ 117 | color: 'pi', 118 | bgColor: 'r', 119 | showPercent: false, 120 | showCount: true, 121 | prefix: 'Processing Fruits', 122 | }); 123 | 124 | pBar.start({ total: arr.length }); 125 | 126 | await aForEach(arr, async (f) => { 127 | pBar.inc({ suffix: f }); 128 | await sleep(500); 129 | }); 130 | 131 | pBar.stop(); 132 | ``` 133 | 134 | ![Count Demo](./assets/count-demo.png) 135 | 136 | --- 137 | 138 | Rendering a plain variant progress bar. 139 | 140 | ```js 141 | import { ProgressBar } from '@opentf/cli-pbar'; 142 | 143 | const pBar = new ProgressBar({ 144 | variant: 'PLAIN', 145 | prefix: 'Downloading', 146 | }); 147 | 148 | pBar.start({ total: 3 }); 149 | pBar.inc(); 150 | pBar.stop(); 151 | ``` 152 | 153 | ![Plain Variant Demo](./assets/plain-demo.png) 154 | 155 | --- 156 | 157 | It does not render progress bars in non TTY terminals, like CI, etc. 158 | 159 | ```js 160 | import { sleep, aForEach } from '@opentf/std'; 161 | import { ProgressBar } from '@opentf/cli-pbar'; 162 | 163 | const arr = ['File 1', 'File 2', 'File 3']; 164 | const pBar = new ProgressBar({ 165 | prefix: 'Downloading', 166 | showPercent: false, 167 | showCount: true, 168 | }); 169 | 170 | pBar.start({ total: arr.length }); 171 | 172 | await aForEach(arr, async (f) => { 173 | pBar.inc({ suffix: f }); 174 | await sleep(500); 175 | }); 176 | 177 | pBar.stop(); 178 | ``` 179 | 180 | ![CI Demo](./assets/ci-demo.png) 181 | 182 | ## API 183 | 184 | ### options: 185 | 186 | | Name | Type | Default | Description | 187 | | ----------- | ----------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 188 | | stream | WriteStream | process.stderr | The [TTY](https://nodejs.org/docs/latest-v20.x/api/tty.html#class-ttywritestream) writable stream to use. | 189 | | width | number | 30 | The size of the progress bar. | 190 | | prefix | string | '' | The string to be prefixed progress bar. | 191 | | suffix | string | '' | The string to be suffixed progress bar. | 192 | | color | string | 'g' | The color to render the completed progress bar.
The default color is `green`.
It uses [@opentf/cli-styles](https://www.npmjs.com/package/@opentf/cli-styles) for colors.
You can also use the `rgb` & `hex` color modes, please refer the [supported color keys here](https://github.com/open-tech-foundation/js-cli-styles#color-keys). | 193 | | bgColor | string | 'gr' | The color to render the incomplete progress bar.
The default color is `grey`.
It uses [@opentf/cli-styles](https://www.npmjs.com/package/@opentf/cli-styles) for colors.
You can also use the `rgb` & `hex` color modes, please refer the [supported color keys here](https://github.com/open-tech-foundation/js-cli-styles#color-keys). | 194 | | size | string | 'DEFAULT' | The size of the progress bar to render.
Available sizes:
'DEFAULT'
'MEDIUM'
'SMALL' | 195 | | autoClear | boolean | false | If true, then it auto-clears the progress bar after the `stop` method is called. | 196 | | showPercent | boolean | true | If false, then it hides the progress bar percent. | 197 | | showCount | boolean | false | If true, then it show the progress bar count. | 198 | | variant | string | 'STANDARD' | There are two variants available, `STANDARD` & `PLAIN`. | 199 | 200 | ### Instance methods: 201 | 202 | **start(obj?: Partial): void** 203 | 204 | After the method is called, the progress bar starts rendering. 205 | 206 | #### Bar: 207 | 208 | | Name | Type | Default | Description | 209 | | ----------- | ------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 210 | | total | number | NaN | The total value for the progress bar. | 211 | | value | number | NaN | The current value of the progress bar. | 212 | | prefix | string | '' | The string to be prefixed progress bar. | 213 | | suffix | string | '' | The string to be suffixed progress bar. | 214 | | color | string | 'g' | The color to render the completed progress bar.
The default color is `green`.
It uses [@opentf/cli-styles](https://www.npmjs.com/package/@opentf/cli-styles) for colors.
You can also use the `rgb` & `hex` color modes, please refer the [supported color keys here](https://github.com/open-tech-foundation/js-cli-styles#color-keys). | 215 | | bgColor | string | 'gr' | The color to render the incomplete progress bar.
The default color is `grey`.
It uses [@opentf/cli-styles](https://www.npmjs.com/package/@opentf/cli-styles) for colors.
You can also use the `rgb` & `hex` color modes, please refer the [supported color keys here](https://github.com/open-tech-foundation/js-cli-styles#color-keys). | 216 | | size | string | 'DEFAULT' | The size of the progress bar.
Available sizes:
'DEFAULT'
'MEDIUM'
'SMALL' | 217 | | progress | boolean | true | If `false`, it does not render a progress bar, making it useful to add an empty line or text without displaying a progress bar. | 218 | | showPercent | boolean | true | If false, then it hides the progress bar percent. | 219 | | showCount | boolean | false | If true, then it show the progress bar count. | 220 | 221 | ### add(bar: Partial): { update: (bar: Partial) => void } 222 | 223 | In `multi-progress`, it appends a progress bar to the container and returns an instance. 224 | 225 | ### update(bar: Partial): void 226 | 227 | It is used to update the current progress bar instance. 228 | 229 | ### inc(bar: Partial): void 230 | 231 | It increments the progress bar value and optionaly updates the other bar props. 232 | 233 | ### stop(msg?: string): void 234 | 235 | Stops the current progress bar instance with the current state and optionally clears the progress bar when `autoClear` is true. 236 | 237 | You can also pass `msg` text to be displayed after the instance stops. 238 | 239 | ## Supported Color Keys 240 | 241 | | Key | Description | 242 | | --- | -------------------------- | 243 | | r | Red - rgb(255,65,54) | 244 | | g | Green - rgb(46,204,64) | 245 | | b | Blue - rgb(0,116,217) | 246 | | o | Orange - rgb(255,133,27) | 247 | | y | Yellow - rgb(255,220,0) | 248 | | w | White - rgb(255,255,255) | 249 | | m | Magenta - rgb(255,105,193) | 250 | | c | Cyan - rgb(154, 236, 254) | 251 | | n | Navy - rgb(0,31,63) | 252 | | a | Aqua - rgb(127,219,255) | 253 | | t | Teal - rgb(57,204,204) | 254 | | p | Purple - rgb(177,13,201) | 255 | | f | Fuchsia - rgb(240,18,190) | 256 | | s | Silver - rgb(221,221,221) | 257 | | ma | Maroon - rgb(133,20,75) | 258 | | ol | Olive - rgb(61,153,112) | 259 | | li | Lime - rgb(1,255,112) | 260 | | bl | Black - rgb(17,17,17) | 261 | | gr | Grey - rgb(170,170,170) | 262 | | pi | Pink - rgb(255, 191, 203) | 263 | 264 | ## Related 265 | 266 | - [@opentf/std](https://js-std.pages.dev/) - An Extensive JavaScript Standard Library. 267 | 268 | - [@opentf/cli-styles](https://github.com/Open-Tech-Foundation/js-cli-styles) - Style your CLI text using ANSI escape sequences. 269 | 270 | ## License 271 | 272 | Copyright (c) 2021, [Thanga Ganapathy](https://thanga-ganapathy.github.io) ([MIT License](./LICENSE)). 273 | -------------------------------------------------------------------------------- /__tests__/multiProgressBar.spec.js: -------------------------------------------------------------------------------- 1 | import { PassThrough } from 'stream'; 2 | import { style } from '@opentf/cli-styles'; 3 | import { ProgressBar } from '../src/index.ts'; 4 | import { 5 | DEFAULT_BAR_CHAR, 6 | MEDIUM_BAR_CHAR, 7 | SMALL_BAR_CHAR, 8 | } from '../src/constants.ts'; 9 | 10 | function getStream(isTTY = true) { 11 | const stream = new PassThrough(); 12 | stream.clearLine = () => true; 13 | stream.cursorTo = () => true; 14 | stream.moveCursor = () => true; 15 | stream.isTTY = isTTY; 16 | 17 | return stream; 18 | } 19 | 20 | async function getOutput(stream) { 21 | const output = []; 22 | 23 | return new Promise((resolve) => { 24 | stream.on('data', (data) => { 25 | output.push(data.toString()); 26 | }); 27 | 28 | stream.on('end', () => { 29 | resolve(output); 30 | }); 31 | }); 32 | } 33 | 34 | async function run(cb, options = {}, isTTY = true) { 35 | const stream = getStream(isTTY); 36 | const outputPromise = getOutput(stream); 37 | const progress = new ProgressBar(Object.assign(options, { stream: stream })); 38 | await cb(progress); 39 | stream.end(); 40 | 41 | return await outputPromise; 42 | } 43 | 44 | function getBars(complete = 0, percent = 0, opt = {}) { 45 | const options = { 46 | width: 30, 47 | color: 'g', 48 | bgColor: 'gr', 49 | size: 'DEFAULT', 50 | prefix: '', 51 | suffix: '', 52 | ...opt, 53 | }; 54 | let barChar; 55 | if (options.size === 'DEFAULT') { 56 | barChar = DEFAULT_BAR_CHAR; 57 | } else if (options.size === 'MEDIUM') { 58 | barChar = MEDIUM_BAR_CHAR; 59 | } else { 60 | barChar = SMALL_BAR_CHAR; 61 | } 62 | return ( 63 | options.prefix + 64 | ' ' + 65 | style(`$${options.color}.bol{${barChar}}`).repeat(complete) + 66 | style(`$${options.bgColor}.dim{${barChar}}`).repeat( 67 | options.width - complete 68 | ) + 69 | ` ${percent}%` + 70 | ' ' + 71 | options.suffix 72 | ); 73 | } 74 | 75 | describe('Multi Progress Bar', () => { 76 | it('renders single bar created using .add methods', async () => { 77 | const output = await run(async (progress) => { 78 | progress.start(); 79 | progress.add({ total: 100 }); 80 | progress.stop(); 81 | }); 82 | const bars = getBars(); 83 | expect(output[0].trim()).toMatch(bars.trim()); 84 | }); 85 | 86 | it('renders two bars', async () => { 87 | const output = await run(async (progress) => { 88 | progress.start(); 89 | progress.add({ total: 100 }); 90 | progress.add({ total: 100 }); 91 | progress.stop(); 92 | }); 93 | const bars = getBars(); 94 | expect(output[0].trim()).toMatch(bars.trim()); 95 | expect(output[1].trim()).toMatch(bars.trim()); 96 | }); 97 | 98 | it('renders three bars with individual progress', async () => { 99 | const output = await run(async (progress) => { 100 | progress.start(); 101 | const b1 = progress.add({ total: 100 }); 102 | const b2 = progress.add({ total: 100 }); 103 | const b3 = progress.add({ total: 100 }); 104 | b1.update({ value: 10 }); 105 | b3.update({ value: 20 }); 106 | b2.update({ value: 50 }); 107 | progress.stop(); 108 | }); 109 | let bars = getBars(); 110 | expect(output[0].trim()).toMatch(bars.trim()); 111 | expect(output[1].trim()).toMatch(bars.trim()); 112 | expect(output[3].trim()).toMatch(bars.trim()); 113 | expect(output[4].trim()).toMatch(bars.trim()); 114 | expect(output[6].trim()).toMatch(bars.trim()); 115 | expect(output[8].trim()).toMatch(bars.trim()); 116 | bars = getBars(3, 10); 117 | expect(output[9].trim()).toMatch(bars.trim()); 118 | bars = getBars(); 119 | expect(output[11].trim()).toMatch(bars.trim()); 120 | expect(output[13].trim()).toMatch(bars.trim()); 121 | bars = getBars(3, 10); 122 | expect(output[14].trim()).toMatch(bars.trim()); 123 | bars = getBars(); 124 | expect(output[16].trim()).toMatch(bars.trim()); 125 | bars = getBars(6, 20); 126 | expect(output[18].trim()).toMatch(bars.trim()); 127 | bars = getBars(3, 10); 128 | expect(output[19].trim()).toMatch(bars.trim()); 129 | bars = getBars(15, 50); 130 | expect(output[21].trim()).toMatch(bars.trim()); 131 | bars = getBars(6, 20); 132 | expect(output[23].trim()).toMatch(bars.trim()); 133 | expect(output[24].trim()).toMatch(''); 134 | expect(output[25]).toBeUndefined(); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /__tests__/progressBar.spec.js: -------------------------------------------------------------------------------- 1 | import { PassThrough } from 'stream'; 2 | import { style } from '@opentf/cli-styles'; 3 | import { ProgressBar } from '../src/index.ts'; 4 | import { 5 | DEFAULT_BAR_CHAR, 6 | MEDIUM_BAR_CHAR, 7 | PLAIN_DONE_BAR_CHAR, 8 | PLAIN_NOT_DONE_BAR_CHAR, 9 | SMALL_BAR_CHAR, 10 | } from '../src/constants.ts'; 11 | 12 | function getStream(isTTY = true) { 13 | const stream = new PassThrough(); 14 | stream.clearLine = () => true; 15 | stream.cursorTo = () => true; 16 | stream.moveCursor = () => true; 17 | stream.isTTY = isTTY; 18 | 19 | return stream; 20 | } 21 | 22 | async function getOutput(stream) { 23 | const output = []; 24 | 25 | return new Promise((resolve) => { 26 | stream.on('data', (data) => { 27 | output.push(data.toString()); 28 | }); 29 | 30 | stream.on('end', () => { 31 | resolve(output); 32 | }); 33 | }); 34 | } 35 | 36 | async function run(cb, options = {}, isTTY = true) { 37 | const stream = getStream(isTTY); 38 | const outputPromise = getOutput(stream); 39 | const progress = new ProgressBar(Object.assign(options, { stream: stream })); 40 | await cb(progress); 41 | stream.end(); 42 | 43 | return await outputPromise; 44 | } 45 | 46 | function getBarsStr(n, done = false) { 47 | return style( 48 | `$${done ? 'g' : 'gr'}.${done ? 'bol' : 'dim'}{${DEFAULT_BAR_CHAR}}` 49 | ).repeat(n); 50 | } 51 | 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 | 60 | function getBars(complete = 0, percent = 0, opt = {}) { 61 | const options = { 62 | width: 30, 63 | color: 'g', 64 | bgColor: 'gr', 65 | size: 'DEFAULT', 66 | prefix: '', 67 | suffix: '', 68 | ...opt, 69 | }; 70 | let barChar; 71 | if (options.size === 'DEFAULT') { 72 | barChar = DEFAULT_BAR_CHAR; 73 | } else if (options.size === 'MEDIUM') { 74 | barChar = MEDIUM_BAR_CHAR; 75 | } else { 76 | barChar = SMALL_BAR_CHAR; 77 | } 78 | return ( 79 | options.prefix + 80 | ' ' + 81 | style(`$${options.color}.bol{${barChar}}`).repeat(complete) + 82 | style(`$${options.bgColor}.dim{${barChar}}`).repeat( 83 | options.width - complete 84 | ) + 85 | ` ${percent}%` + 86 | ' ' + 87 | options.suffix 88 | ); 89 | } 90 | 91 | describe('Single Progress Bar', () => { 92 | it('renders default 0%', async () => { 93 | const output = await run(async (progress) => { 94 | progress.start({ total: 100 }); 95 | progress.stop(); 96 | }); 97 | const bars = getBars(); 98 | expect(output[0].trim()).toMatch(bars.trim()); 99 | }); 100 | 101 | it('renders default 10%', async () => { 102 | const output = await run(async (progress) => { 103 | progress.start({ total: 100 }); 104 | progress.update({ value: 10 }); 105 | progress.stop(); 106 | }); 107 | const bars = getBars(3, 10); 108 | expect(output[1].trim()).toMatch(bars.trim()); 109 | }); 110 | 111 | it('renders default 50%', async () => { 112 | const output = await run(async (progress) => { 113 | progress.start({ total: 100 }); 114 | progress.update({ value: 50 }); 115 | progress.stop(); 116 | }); 117 | const bars = getBars(15, 50); 118 | expect(output[1].trim()).toMatch(bars.trim()); 119 | }); 120 | 121 | it('renders default 100%', async () => { 122 | const output = await run(async (progress) => { 123 | progress.start({ total: 100 }); 124 | progress.update({ value: 100 }); 125 | progress.stop(); 126 | }); 127 | const bars = getBars(30, 100); 128 | expect(output[1].trim()).toMatch(bars.trim()); 129 | }); 130 | 131 | it('renders medium size 10% with blue color', async () => { 132 | const output = await run( 133 | async (progress) => { 134 | progress.start({ total: 100 }); 135 | progress.update({ value: 10 }); 136 | progress.stop(); 137 | }, 138 | { size: 'MEDIUM', color: 'b' } 139 | ); 140 | const bars = getBars(3, 10, { size: 'MEDIUM', color: 'b' }); 141 | expect(output[1].trim()).toMatch(bars.trim()); 142 | }); 143 | 144 | it('renders small size 75% with red color', async () => { 145 | const output = await run( 146 | async (progress) => { 147 | progress.start({ total: 100 }); 148 | progress.update({ value: 75 }); 149 | progress.stop(); 150 | }, 151 | { size: 'SMALL', color: 'r' } 152 | ); 153 | const bars = getBars(22, 75, { size: 'SMALL', color: 'r' }); 154 | expect(output[1].trim()).toMatch(bars.trim()); 155 | }); 156 | 157 | it('renders red bg color', async () => { 158 | const output = await run( 159 | async (progress) => { 160 | progress.start({ total: 100 }); 161 | progress.update({ value: 50 }); 162 | progress.stop(); 163 | }, 164 | { bgColor: 'r' } 165 | ); 166 | const bars = getBars(15, 50, { bgColor: 'r' }); 167 | expect(output[1].trim()).toMatch(bars.trim()); 168 | }); 169 | 170 | it('renders width 50', async () => { 171 | const output = await run( 172 | async (progress) => { 173 | progress.start({ total: 100 }); 174 | progress.update({ value: 50 }); 175 | progress.stop(); 176 | }, 177 | { width: 50 } 178 | ); 179 | const bars = getBars(25, 50, { width: 50 }); 180 | expect(output[1].trim()).toMatch(bars.trim()); 181 | }); 182 | 183 | it('renders with prefix', async () => { 184 | const output = await run(async (progress) => { 185 | progress.start({ total: 100, prefix: 'PREFIX' }); 186 | progress.stop(); 187 | }); 188 | const bars = getBars(0, 0, { prefix: 'PREFIX' }); 189 | expect(output[0].trim()).toMatch(bars.trim()); 190 | }); 191 | 192 | it('renders with suffix', async () => { 193 | const output = await run(async (progress) => { 194 | progress.start({ total: 100, prefix: 'SUFFIX' }); 195 | progress.stop(); 196 | }); 197 | const bars = getBars(0, 0, { prefix: 'SUFFIX' }); 198 | expect(output[0].trim()).toMatch(bars.trim()); 199 | }); 200 | 201 | it('renders with prefix & suffix', async () => { 202 | const output = await run(async (progress) => { 203 | progress.start({ total: 100, prefix: 'SUFFIX', suffix: 'SUFFIX' }); 204 | progress.stop(); 205 | }); 206 | const bars = getBars(0, 0, { prefix: 'SUFFIX', suffix: 'SUFFIX' }); 207 | expect(output[0].trim()).toMatch(bars.trim()); 208 | }); 209 | 210 | it('renders with no precent & with count', async () => { 211 | const output = await run( 212 | async (pBar) => { 213 | pBar.start({ total: 3 }); 214 | pBar.inc(); 215 | pBar.inc(); 216 | pBar.stop(); 217 | }, 218 | { showPercent: false, width: 3, showCount: true } 219 | ); 220 | const outBars = getBarsStr(1, true) + getBarsStr(2) + ' [2/3]'; 221 | expect(output[2]).toStrictEqual(outBars); 222 | }); 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 | }); 272 | }); 273 | -------------------------------------------------------------------------------- /assets/ci-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Open-Tech-Foundation/cli-pbar/f75676679fd2b29649920a3c728e40418b5731bf/assets/ci-demo.png -------------------------------------------------------------------------------- /assets/count-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Open-Tech-Foundation/cli-pbar/f75676679fd2b29649920a3c728e40418b5731bf/assets/count-demo.png -------------------------------------------------------------------------------- /assets/demo-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Open-Tech-Foundation/cli-pbar/f75676679fd2b29649920a3c728e40418b5731bf/assets/demo-min.gif -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Open-Tech-Foundation/cli-pbar/f75676679fd2b29649920a3c728e40418b5731bf/assets/demo.gif -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Open-Tech-Foundation/cli-pbar/f75676679fd2b29649920a3c728e40418b5731bf/assets/demo.png -------------------------------------------------------------------------------- /assets/plain-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Open-Tech-Foundation/cli-pbar/f75676679fd2b29649920a3c728e40418b5731bf/assets/plain-demo.png -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | import https from 'node:https'; 2 | import path from 'node:path'; 3 | import { aForEach, sleep } from '@opentf/std'; 4 | import { style } from '@opentf/cli-styles'; 5 | import { ProgressBar } from './src'; 6 | 7 | function template(title, options = {}) { 8 | console.log(style(`$o.bol.und{\n${title}\n}`)); 9 | return new ProgressBar(options); 10 | } 11 | 12 | function defaultBar() { 13 | return new Promise((resolve) => { 14 | const pBar = template('Default'); 15 | pBar.start({ total: 100 }); 16 | let value = 0; 17 | const intervalID = setInterval(() => { 18 | value += 10; 19 | pBar.update({ value }); 20 | if (value === 100) { 21 | pBar.stop(); 22 | clearInterval(intervalID); 23 | resolve(); 24 | } 25 | }, 100); 26 | }); 27 | } 28 | 29 | function mediumBar() { 30 | return new Promise((resolve) => { 31 | const pBar = template('Medium size + Blue bar', { 32 | size: 'MEDIUM', 33 | color: 'b', 34 | }); 35 | pBar.start({ total: 100 }); 36 | let value = 0; 37 | const intervalID = setInterval(() => { 38 | value += 10; 39 | pBar.update({ value }); 40 | if (value === 100) { 41 | pBar.stop(); 42 | clearInterval(intervalID); 43 | resolve(); 44 | } 45 | }, 100); 46 | }); 47 | } 48 | 49 | function smallBar() { 50 | return new Promise((resolve) => { 51 | const pBar = template('Small size + Red bar', { 52 | size: 'SMALL', 53 | color: 'r', 54 | }); 55 | pBar.start({ total: 100 }); 56 | let value = 0; 57 | const intervalID = setInterval(() => { 58 | value += 10; 59 | pBar.update({ value }); 60 | if (value === 100) { 61 | pBar.stop(); 62 | clearInterval(intervalID); 63 | resolve(); 64 | } 65 | }, 100); 66 | }); 67 | } 68 | 69 | function prefixSuffix() { 70 | return new Promise((resolve) => { 71 | const pBar = template('Prefix + Bar + Suffix'); 72 | pBar.start({ total: 100, prefix: 'Installing npm pkgs' }); 73 | const pkgs = ['babel', 'rollup', 'webpack', 'vite', 'react']; 74 | let value = 0; 75 | const intervalID = setInterval(() => { 76 | value += 20; 77 | pBar.update({ value, suffix: pkgs.pop() }); 78 | if (value === 100) { 79 | pBar.update({ prefix: '', suffix: 'Install completed.' }); 80 | pBar.stop(); 81 | clearInterval(intervalID); 82 | resolve(); 83 | } 84 | }, 500); 85 | }); 86 | } 87 | 88 | async function downloading() { 89 | const files = [ 90 | 'https://nodejs.org/dist/v16.0.0/node-v16.0.0-x64.msi', 91 | 'https://nodejs.org/dist/v18.16.1/node-v18.16.1-x64.msi', 92 | 'https://nodejs.org/dist/v20.0.0/node-v20.0.0-x64.msi', 93 | ]; 94 | 95 | function download(url, pBar) { 96 | return new Promise((resolve) => { 97 | let bar; 98 | let value = 0; 99 | https 100 | .get(url, (res) => { 101 | bar = pBar.add({ 102 | total: +res.headers['content-length'], 103 | }); 104 | res.on('data', (chunk) => { 105 | value += chunk.length; 106 | bar.update({ value, suffix: path.basename(url) }); 107 | }); 108 | res.on('end', () => { 109 | resolve(); 110 | }); 111 | }) 112 | .on('error', (e) => { 113 | console.error(`Got error: ${e.message}`); 114 | }); 115 | }); 116 | } 117 | 118 | console.log('Downloading files:'); 119 | const downloadBar = template('Multi Progressbar', { size: 'MEDIUM' }); 120 | downloadBar.start(); 121 | await Promise.all(files.map((f) => download(f, downloadBar))); 122 | downloadBar.stop(); 123 | } 124 | 125 | async function autoClear() { 126 | const pBar = template('Multi + Auto Clear + Stop msg', { autoClear: true }); 127 | console.log(); 128 | console.log('Compiling web application...'); 129 | console.log(); 130 | pBar.start(); 131 | const client = pBar.add({ 132 | total: 100, 133 | color: 'rgb(0,181,34)', 134 | prefix: style('$rgb(0,181,34){● Client}'), 135 | }); 136 | const cfiles = pBar.add({ 137 | progress: false, 138 | }); 139 | pBar.add({ 140 | progress: false, 141 | }); 142 | const server = pBar.add({ 143 | total: 100, 144 | color: 'hex(#FFB12B)', 145 | prefix: style('$hex(#FFB12B){● Server}'), 146 | }); 147 | const sfiles = pBar.add({ 148 | progress: false, 149 | }); 150 | const pkgs = [ 151 | 'react', 152 | 'zustand', 153 | 'recoil', 154 | 'lodash', 155 | '@opentf/std', 156 | 'emotion', 157 | 'material-ui', 158 | 'pandacss', 159 | 'postcss', 160 | 'tailwind', 161 | ]; 162 | const sFiles = ['login', 'logout', 'products', 'dashboard', 'authorize']; 163 | let cVal = 0; 164 | let sVal = 0; 165 | const intervalID = setInterval(() => { 166 | if (cVal < 100) { 167 | cVal += 10; 168 | client.update({ value: cVal }); 169 | cfiles.update({ 170 | suffix: `/node_modules/${pkgs.pop()}/index.js`, 171 | }); 172 | } 173 | 174 | if (sVal < 100) { 175 | sVal += 20; 176 | server.update({ value: sVal }); 177 | sfiles.update({ suffix: `/api/${sFiles.pop()}.js` }); 178 | } 179 | 180 | if (sVal === 100) { 181 | sfiles.update({ suffix: '' }); 182 | } 183 | 184 | if (cVal === 100) { 185 | cfiles.update({ suffix: '' }); 186 | } 187 | 188 | if (cVal === 100 && sVal === 100) { 189 | pBar.stop('Successfully compiled, your app is now ready to deploy 🚀'); 190 | clearInterval(intervalID); 191 | } 192 | }, 100); 193 | 194 | await sleep(1500); 195 | } 196 | 197 | async function styledTexts() { 198 | const multiPBar = template('Styled Texts', {}); 199 | multiPBar.start(); 200 | const b1 = multiPBar.add({ total: 100 }); 201 | const t1 = multiPBar.add({ progress: false }); 202 | const b2 = multiPBar.add({ total: 100 }); 203 | const t2 = multiPBar.add({ progress: false }); 204 | const b3 = multiPBar.add({ total: 100 }); 205 | const t3 = multiPBar.add({ progress: false }); 206 | b1.update({ value: 23, prefix: 'ProgressBar 1', color: 'g' }); 207 | t1.update({ prefix: style('$r{This is some long text}') }); 208 | b3.update({ 209 | value: 35, 210 | prefix: 'Prefix 3', 211 | suffix: 'Suffix 3', 212 | color: 'b', 213 | bgColor: 'y', 214 | }); 215 | t2.update({ prefix: 'This is some long text', color: 'bl', bgColor: 'y' }); 216 | b2.update({ value: 17, suffix: 'Suffix 2' }); 217 | t3.update({ 218 | prefix: 'This is some long text with invalid styles.', 219 | color: 'bl2', 220 | bgColor: 'y3', 221 | }); 222 | multiPBar.stop(); 223 | } 224 | 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 | 244 | await defaultBar(); 245 | await mediumBar(); 246 | await smallBar(); 247 | await prefixSuffix(); 248 | // await downloading(); 249 | await autoClear(); 250 | await styledTexts(); 251 | await plain(); 252 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | transform: { 3 | '^.+\\.(t|j)sx?$': '@swc/jest', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opentf/cli-pbar", 3 | "version": "0.7.2", 4 | "description": "The Customizable CLI Progress Bars.", 5 | "keywords": [ 6 | "cli", 7 | "progress", 8 | "progressbar", 9 | "bar", 10 | "multi", 11 | "multibar", 12 | "multiple", 13 | "console", 14 | "terminal", 15 | "color", 16 | "colour", 17 | "small" 18 | ], 19 | "type": "module", 20 | "main": "dist/index.cjs", 21 | "module": "dist/index.js", 22 | "types": "dist/index.d.ts", 23 | "exports": { 24 | ".": { 25 | "import": "./dist/index.js", 26 | "require": "./dist/index.cjs", 27 | "types": "./dist/index.d.ts" 28 | } 29 | }, 30 | "scripts": { 31 | "dev": "tsup --watch", 32 | "build": "tsup", 33 | "test": "jest", 34 | "lint": "eslint src/** --fix", 35 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 36 | "type-check": "tsc", 37 | "ci": "pnpm run build && pnpm run test && pnpm run lint && pnpm run type-check", 38 | "prepare": "husky install", 39 | "publish-packages": "pnpm run build && pnpm run test && changeset version && changeset publish" 40 | }, 41 | "repository": "git@github.com:Open-Tech-Foundation/cli-pbar.git", 42 | "author": { 43 | "name": "Thanga Ganapathy", 44 | "email": "ganapathy888@gmail.com", 45 | "url": "https://thanga-ganapathy.github.io" 46 | }, 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/open-tech-foundation/cli-pbar/issues" 50 | }, 51 | "homepage": "https://github.com/open-tech-foundation/cli-pbar#readme", 52 | "lint-staged": { 53 | "*.{ts,tsx}": [ 54 | "pnpm run format", 55 | "pnpm run lint" 56 | ], 57 | "*.json": [ 58 | "npm run format" 59 | ] 60 | }, 61 | "files": [ 62 | "dist" 63 | ], 64 | "devDependencies": { 65 | "@changesets/cli": "^2.27.1", 66 | "@commitlint/cli": "^19.2.1", 67 | "@commitlint/config-conventional": "^19.1.0", 68 | "@opentf/eslint-config-base": "^0.2.0", 69 | "@swc/core": "^1.4.8", 70 | "@swc/jest": "^0.2.24", 71 | "@types/jest": "^29.5.2", 72 | "@types/node": "^20.12.3", 73 | "eslint": "^8.57.0", 74 | "husky": "^9.0.11", 75 | "jest": "^29.7.0", 76 | "lint-staged": "^15.2.2", 77 | "tsup": "^8.0.2", 78 | "turbo": "^1.13.0", 79 | "typescript": "^5.4.3" 80 | }, 81 | "dependencies": { 82 | "@opentf/cli-styles": "^0.15.0", 83 | "@opentf/std": "^0.5.0" 84 | }, 85 | "publishConfig": { 86 | "access": "public", 87 | "provenance": true 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/ProgressBar.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from 'node:os'; 2 | import { style } from '@opentf/cli-styles'; 3 | import { 4 | compact, 5 | intersperse, 6 | isObj, 7 | percentage, 8 | percentageOf, 9 | shallowMerge, 10 | } from '@opentf/std'; 11 | import { type Bar, type BarSize, type Options } from './types'; 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'; 19 | 20 | class ProgressBar { 21 | private _options: Options = { 22 | stream: process.stderr, 23 | width: 30, 24 | color: 'g', 25 | bgColor: 'gr', 26 | autoClear: false, 27 | size: 'DEFAULT', 28 | prefix: '', 29 | suffix: '', 30 | showPercent: true, 31 | showCount: false, 32 | variant: 'STANDARD', 33 | }; 34 | private _bars: Bar[]; 35 | 36 | constructor(options?: Partial) { 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; 42 | this._bars = []; 43 | } 44 | 45 | private _getBarCharBySize(size: BarSize | undefined) { 46 | const s = size || this._options.size; 47 | if (s === 'SMALL') { 48 | return SMALL_BAR_CHAR; 49 | } else if (s === 'MEDIUM') { 50 | return MEDIUM_BAR_CHAR; 51 | } 52 | 53 | return DEFAULT_BAR_CHAR; 54 | } 55 | 56 | private _getBars(bar: Bar, percent: number): string { 57 | let doneBars, bgBars; 58 | const color = bar.color || this._options.color; 59 | const bgColor = bar.bgColor || this._options.bgColor; 60 | const percentVal = Math.trunc(percentageOf(percent, this._options.width)); 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 | 77 | return doneBars + bgBars; 78 | } 79 | 80 | private _render() { 81 | this._bars.forEach((b, i) => { 82 | let str = ''; 83 | 84 | if (i > 0 && this._options.stream.isTTY) { 85 | this._options.stream.write(EOL); 86 | } 87 | 88 | const prefix = Object.hasOwn(b, 'prefix') 89 | ? b.prefix 90 | : this._options.prefix; 91 | const suffix = Object.hasOwn(b, 'suffix') 92 | ? b.suffix 93 | : this._options.suffix; 94 | const showPercent = Object.hasOwn(b, 'showPercent') 95 | ? b.showPercent 96 | : this._options.showPercent; 97 | const showCount = Object.hasOwn(b, 'showCount') 98 | ? b.showCount 99 | : this._options.showCount; 100 | const variant = Object.hasOwn(b, 'variant') 101 | ? b.variant 102 | : this._options.variant; 103 | 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) { 109 | const bar = this._getBars(b, percent); 110 | str += ( 111 | intersperse( 112 | compact([ 113 | prefix, 114 | variant === 'PLAIN' ? `[${bar}]` : bar, 115 | showPercent ? percent + '%' : null, 116 | showCount ? `[${b.value || 0}/${b.total || 0}]` : null, 117 | suffix, 118 | ]), 119 | ' ' 120 | ) as string[] 121 | ).join(''); 122 | } else { 123 | str += prefix + ' ' + suffix; 124 | } 125 | 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 | 143 | if ( 144 | this._options.stream.cursorTo(0) && 145 | this._options.stream.clearLine(0) 146 | ) { 147 | this._options.stream.write(str); 148 | } 149 | }); 150 | } 151 | 152 | /** Starts rendering of the progress bars. */ 153 | start(bar?: Partial) { 154 | if (isObj(bar)) { 155 | this._bars.push({ ...bar, progress: true } as Bar); 156 | } 157 | this._render(); 158 | } 159 | 160 | private _clear() { 161 | this._options.stream.moveCursor(0, -(this._bars.length - 1)); 162 | this._options.stream.cursorTo(0); 163 | this._options.stream.clearScreenDown(); 164 | } 165 | 166 | /** Stops the current progress bar with optional message to display. */ 167 | stop(msg?: string) { 168 | if (this._options.autoClear) { 169 | this._clear(); 170 | if (msg) { 171 | this._options.stream.write(msg + EOL); 172 | } 173 | return; 174 | } 175 | this._options.stream.write(EOL); 176 | } 177 | 178 | /** Update the current progress bar */ 179 | update(bar: Partial, id?: number) { 180 | if (!id) { 181 | this._bars[0] = { ...(this._bars[0] as Bar), ...bar } as Bar; 182 | } else { 183 | const index = this._bars.findIndex((b) => isObj(b) && b.id === id); 184 | this._bars[index] = { ...(this._bars[index] as Bar), ...bar } as Bar; 185 | } 186 | this._options.stream.moveCursor(0, -(this._bars.length - 1)); 187 | this._render(); 188 | } 189 | 190 | /** Adds a new progress bar to the rendering stack. */ 191 | add(bar: Partial) { 192 | const id = this._bars.length + 1; 193 | const barInstance = { progress: true, ...bar, id } as Bar; 194 | this._bars.push(barInstance); 195 | if (this._bars.length > 2) { 196 | this._options.stream.moveCursor(0, -(this._bars.length - 2)); 197 | } 198 | this._render(); 199 | return { 200 | update: (bar: Partial) => { 201 | this.update(bar, id); 202 | }, 203 | inc: (bar?: Partial, val = 1) => { 204 | const curBar = this._bars.find((b) => b.id === id); 205 | const n = curBar?.value || 0; 206 | this.update({ ...bar, value: n + val }, id); 207 | }, 208 | }; 209 | } 210 | 211 | /** Increment the value + 1, optionally with the provided value. */ 212 | inc(bar?: Partial, val = 1) { 213 | const n = this._bars[0]?.value || 0; 214 | this.update({ ...bar, value: n + val }); 215 | } 216 | } 217 | 218 | export default ProgressBar; 219 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BAR_CHAR = '\u{2588}'; 2 | 3 | export const SMALL_BAR_CHAR = '\u{2501}'; 4 | 5 | export const MEDIUM_BAR_CHAR = '\u{2586}'; 6 | 7 | export const PLAIN_DONE_BAR_CHAR = '='; 8 | 9 | export const PLAIN_NOT_DONE_BAR_CHAR = '-'; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from './ProgressBar'; 2 | 3 | export { ProgressBar }; 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { WriteStream } from 'node:tty'; 2 | 3 | export type BarSize = 'SMALL' | 'MEDIUM' | 'DEFAULT'; 4 | 5 | export type Options = { 6 | /** The wriatable stream to use, default is stderr. */ 7 | stream: WriteStream; 8 | /** The width of the rendered progress bar. */ 9 | width: number; 10 | /** The foreground color of completed bars. */ 11 | color: string; 12 | /** The background color of bars. */ 13 | bgColor: string; 14 | /** Set true to remove the progress bar after it completed. */ 15 | autoClear: boolean; 16 | /** The size of the progress bar to render, default is 30. */ 17 | size: BarSize; 18 | /** The string to be prefixed progress bar */ 19 | prefix: string; 20 | /** The string to be suffixed progress bar */ 21 | suffix: string; 22 | /** Show hide progress bar percent */ 23 | showPercent: boolean; 24 | /** Show hide progress bar count */ 25 | showCount: boolean; 26 | /** Renders the progress bars based on the variant */ 27 | variant: 'PLAIN' | 'STANDARD'; 28 | }; 29 | 30 | export type Bar = Omit & { 31 | id?: number; 32 | total: number; 33 | value: number; 34 | progress?: boolean; 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "ignoreDeprecations": "5.0", 4 | "noEmit": true, 5 | "rootDir": "./src", 6 | "types": ["node"], 7 | "lib": ["es2022"], 8 | "module": "ESNext", 9 | "target": "es2022", 10 | "strict": true, 11 | "moduleResolution": "node", 12 | "verbatimModuleSyntax": true, 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "exactOptionalPropertyTypes": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitOverride": true, 18 | "noImplicitReturns": true, 19 | "noPropertyAccessFromIndexSignature": true, 20 | "noUncheckedIndexedAccess": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "checkJs": true, 24 | "esModuleInterop": true, 25 | "skipLibCheck": true, 26 | "forceConsistentCasingInFileNames": true 27 | }, 28 | "include": ["src/**/*"] 29 | } 30 | -------------------------------------------------------------------------------- /tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: true, 8 | format: ['cjs', 'esm'], 9 | dts: true, 10 | minify: true, 11 | }); 12 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "outputs": ["dist/**"] 6 | }, 7 | "lint": {}, 8 | "test": {} 9 | }, 10 | "globalEnv": [] 11 | } 12 | --------------------------------------------------------------------------------