├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── changelog.md ├── deno.json ├── deps.example.ts ├── deps.test.ts ├── deps.ts ├── examples ├── backward.ts ├── changeBgColor.ts ├── changeColor.ts ├── clear.ts ├── colorProgression.ts ├── complete.ts ├── console.ts ├── display.ts ├── eta.ts ├── info.ts ├── multi.ts ├── multi2.ts ├── multiPrettyTime.ts ├── preciseBar.ts ├── prettyTime.ts ├── stderr.ts ├── title.ts ├── total.ts └── width.ts ├── mod.ts ├── multi.ts ├── screenshots ├── backward.gif ├── changeBgColor.gif ├── changeColor.gif ├── clear.gif ├── colorProgression.gif ├── complete.gif ├── console.gif ├── display.gif ├── info.gif ├── logo.png ├── multi.gif ├── preciseBar.gif ├── title.gif ├── total.gif └── width.gif ├── tests ├── mod.test.ts ├── multi.test.ts └── time.test.ts └── time.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: deno-library # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will install Deno then run `deno lint` and `deno test`. 7 | # For more information see: https://github.com/denoland/setup-deno 8 | 9 | name: Publish 10 | 11 | on: 12 | push: 13 | branches: ["master"] 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | 19 | permissions: 20 | contents: read 21 | id-token: write 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Publish package 27 | run: npx jsr publish 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./test 2 | registries 3 | deps -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 zfx 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 | # ProgressBar 2 | 3 | ProgressBar in terminal for deno 4 | 5 | [![JSR Version](https://jsr.io/badges/@deno-library/progress)](https://jsr.io/@deno-library/progress) 6 | [![deno.land/x/progress](https://deno.land/badge/progress/version)](https://deno.land/x/progress) 7 | [![LICENSE](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/deno-library/progress/blob/main/LICENSE) 8 | 9 | ![logo](screenshots/logo.png) 10 | 11 | ## Changelog 12 | 13 | [changelog](./changelog.md) 14 | 15 | ## Usage 16 | 17 | ### Multiple progress bars 18 | 19 | #### example 20 | 21 | ```ts 22 | import { MultiProgressBar } from "jsr:@deno-library/progress"; 23 | import { delay } from "jsr:@std/async"; 24 | 25 | // or JSR (with version) 26 | // import { MultiProgressBar } from "jsr:@deno-library/progress@1.5.1"; 27 | // import { delay } from "jsr:@std/async@0.221.0"; 28 | 29 | // or JSR (no prefix, run `deno add @deno-library/progress` and `deno add @std/async`) 30 | // import { MultiProgressBar } from "@deno-library/progress"; 31 | // import { delay } from "@std/async"; 32 | 33 | // or 34 | // import { MultiProgressBar } from "https://deno.land/x/progress@v1.5.1/mod.ts"; 35 | // import { delay } from "https://deno.land/std@0.220.1/async/delay.ts"; 36 | 37 | const title = "download files"; 38 | const total = 100; 39 | 40 | const bars = new MultiProgressBar({ 41 | title, 42 | // clear: true, 43 | complete: "=", 44 | incomplete: "-", 45 | display: "[:bar] :text :percent :time :completed/:total", 46 | }); 47 | 48 | let completed1 = 0; 49 | let completed2 = 0; 50 | 51 | async function download() { 52 | while (completed1 <= total || completed2 <= total) { 53 | completed1 += 1; 54 | completed2 += 2; 55 | await bars.render([ 56 | { 57 | completed: completed1, 58 | total, 59 | text: "file1", 60 | complete: "*", 61 | incomplete: ".", 62 | }, 63 | { completed: completed2, total, text: "file2" }, 64 | ]); 65 | 66 | await delay(50); 67 | } 68 | } 69 | 70 | await download(); 71 | ``` 72 | 73 | #### interface 74 | 75 | ```ts 76 | interface constructorOptions { 77 | title?: string; 78 | width?: number; 79 | complete?: string; 80 | incomplete?: string; 81 | clear?: boolean; 82 | interval?: number; 83 | display?: string; 84 | prettyTime?: boolean; 85 | output?: typeof Deno.stdout | typeof Deno.stderr; 86 | } 87 | 88 | interface renderOptions { 89 | completed: number; 90 | text?: string; 91 | total?: number; 92 | complete?: string; 93 | incomplete?: string; 94 | prettyTimeOptions?: prettyTimeOptions; 95 | } 96 | 97 | /** 98 | * prettyTime options 99 | * @param withSpaces Whether to use spaces to separate times, `1d2h3m5s` or `1d 2h 3m 5s`, default false 100 | * @param toFixedVal value pass to toFixed for seconds, default 1 101 | * @param longFormat Whether to use a long format, default false, `1d2h3m5s` or `1days 2hours 3minutes 5seconds` 102 | */ 103 | interface prettyTimeOptions { 104 | withSpaces?: boolean; 105 | toFixedVal?: number; 106 | longFormat?: boolean; 107 | } 108 | 109 | class MultiProgressBar { 110 | /** 111 | * Title, total, complete, incomplete, can also be set or changed in the render method 112 | * 113 | * @param title Progress bar title, default: '' 114 | * @param width the displayed width of the progress, default: 50 115 | * @param complete completion character, default: colors.bgGreen(' '), can use any string 116 | * @param incomplete incomplete character, default: colors.bgWhite(' '), can use any string 117 | * @param clear clear the bar on completion, default: false 118 | * @param interval minimum time between updates in milliseconds, default: 16 119 | * @param display What is displayed and display order, default: ':bar :text :percent :time :completed/:total' 120 | * @param prettyTime Whether to pretty print time and eta 121 | * @param output Output stream, can be Deno.stdout or Deno.stderr, default is Deno.stdout 122 | */ 123 | constructor(options: ConstructorOptions); 124 | 125 | /** 126 | * "render" the progress bar 127 | * 128 | * @param bars progress bars 129 | * @param bars.completed` completed value 130 | * @param bars.total optional, total number of ticks to complete, default: 100 131 | * @param bars.text optional, text displayed per ProgressBar, default: '' 132 | * @param bars.complete optional, completion character 133 | * @param bars.incomplete optional, incomplete character 134 | * @param bars.prettyTimeOptions optional, prettyTime options 135 | */ 136 | render(bars: Array): Promise; 137 | 138 | /** 139 | * console: interrupt the progress bar and write a message above it 140 | * 141 | * @param message The message to write 142 | */ 143 | console(message: string): Promise; 144 | 145 | /** 146 | * end: end a progress bar. 147 | * No need to call in most cases, unless you want to end before 100% 148 | */ 149 | end(): Promise; 150 | } 151 | ``` 152 | 153 | #### display 154 | 155 | What is displayed and display order, default: ':bar :text :percent :time 156 | :completed/:total' 157 | 158 | - `:bar` the progress bar itself 159 | - `:text` text displayed per ProgressBar 160 | - `:percent` completion percentage 161 | - `:time` time elapsed in seconds 162 | - `:eta` estimated completion time in seconds 163 | - `:total` total number of ticks to complete 164 | - `:completed` completed value 165 | 166 | ### Single progress bar 167 | 168 | #### simple example 169 | 170 | ```ts 171 | import ProgressBar from "jsr:@deno-library/progress"; 172 | import { delay } from "jsr:@std/async"; 173 | 174 | // or JSR (with version) 175 | // import ProgressBar from "jsr:@deno-library/progress@1.5.1"; 176 | // import { delay } from "jsr:@std/async@0.221.0"; 177 | 178 | // or JSR (no prefix, run `deno add @deno-library/progress` and `deno add @std/async`) 179 | // import ProgressBar from "@deno-library/progress"; 180 | // import { delay } from "@std/async"; 181 | 182 | // or 183 | // import ProgressBar from "https://deno.land/x/progress@v1.5.1/mod.ts"; 184 | // import { delay } from "@std/async/delay"; 185 | 186 | const title = "downloading:"; 187 | const total = 100; 188 | const progress = new ProgressBar({ 189 | title, 190 | total, 191 | }); 192 | let completed = 0; 193 | async function download() { 194 | while (completed <= total) { 195 | await progress.render(completed++); 196 | 197 | await delay(50); 198 | } 199 | } 200 | await download(); 201 | ``` 202 | 203 | #### complex example 204 | 205 | ```ts 206 | import ProgressBar from "jsr:@deno-library/progress"; 207 | import { delay } from "jsr:@std/async"; 208 | 209 | // or JSR (with version) 210 | // import ProgressBar from "jsr:@deno-library/progress@1.5.1"; 211 | // import { delay } from "jsr:@std/async@0.221.0"; 212 | 213 | // or JSR (no prefix, run `deno add @deno-library/progress` and `deno add @std/async`) 214 | // import ProgressBar from "@deno-library/progress"; 215 | // import { delay } from "@std/async"; 216 | 217 | // or 218 | // import ProgressBar from "https://deno.land/x/progress@v1.5.1/mod.ts"; 219 | // import { delay } from "@std/async/delay"; 220 | 221 | const total = 100; 222 | const progress = new ProgressBar({ 223 | total, 224 | complete: "=", 225 | incomplete: "-", 226 | display: ":completed/:total hello :time [:bar] :percent", 227 | // or => 228 | // display: ':bar' 229 | // display: ':bar :time' 230 | // display: '[:bar]' 231 | // display: 'hello :bar world' 232 | // ... 233 | }); 234 | let completed = 0; 235 | async function download() { 236 | while (completed <= total) { 237 | await progress.render(completed++); 238 | 239 | await delay(50); 240 | } 241 | } 242 | await download(); 243 | ``` 244 | 245 | More examples in the `examples` folder. 246 | 247 | #### interface 248 | 249 | ```ts 250 | interface ConstructorOptions { 251 | title?: string, 252 | total?: number, 253 | width?: number, 254 | complete?: string, 255 | preciseBar?: string[], 256 | incomplete?: string, 257 | clear?: boolean, 258 | interval?: number, 259 | display?: string 260 | prettyTime?: boolean; 261 | output?: typeof Deno.stdout | typeof Deno.stderr; 262 | } 263 | 264 | interface renderOptions { 265 | title?: string, 266 | total?: number, 267 | text?: string; 268 | complete?: string, 269 | preciseBar?: string[], 270 | incomplete?: string, 271 | prettyTimeOptions?: prettyTimeOptions; 272 | } 273 | 274 | /** 275 | * prettyTime options 276 | * @param withSpaces Whether to use spaces to separate times, `1d2h3m5s` or `1d 2h 3m 5s`, default false 277 | * @param toFixedVal value pass to toFixed for seconds, default 1 278 | * @param longFormat Whether to use a long format, default false, `1d2h3m5s` or `1days 2hours 3minutes 5seconds` 279 | */ 280 | interface prettyTimeOptions { 281 | withSpaces?: boolean; 282 | toFixedVal?: number; 283 | longFormat?: boolean; 284 | } 285 | 286 | class ProgressBar { 287 | /** 288 | * Title, total, complete, incomplete, can also be set or changed in the render method 289 | * 290 | * @param title progress bar title, default: '' 291 | * @param total total number of ticks to complete 292 | * @param width the displayed width of the progress, default: 50 293 | * @param complete completion character, default: colors.bgGreen(' '), can use any string 294 | * @param preciseBar in between character, default: [colors.bgGreen(' ')], can use any string array 295 | * @param incomplete incomplete character, default: colors.bgWhite(' '), can use any string 296 | * @param clear clear the bar on completion, default: false 297 | * @param interval minimum time between updates in milliseconds, default: 16 298 | * @param display What is displayed and display order, default: ':title :percent :bar :time :completed/:total' 299 | * @param prettyTime Whether to pretty print time and eta 300 | * @param output Output stream, can be Deno.stdout or Deno.stderr, default is Deno.stdout 301 | */ 302 | constructor(options: ConstructorOptions): void; 303 | 304 | /** 305 | * render: render the progress bar 306 | * 307 | * @param completed completed value 308 | * @param options optional parameters 309 | * @param options.title optional, progress bar title 310 | * @param options.text optional, custom text, default: '' 311 | * @param options.total optional, total number of ticks to complete, default: 100 312 | * @param options.complete optional, completion character, If you want to change at a certain moment. For example, it turns red at 20% 313 | * @param options.incomplete optional, incomplete character, If you want to change at a certain moment. For example, it turns red at 20% 314 | * @param options.prettyTimeOptions optional, prettyTime options 315 | */ 316 | render(completed: number, options? renderOptions): Promise; 317 | 318 | /** 319 | * console: interrupt the progress bar and write a message above it 320 | * 321 | * @param message The message to write 322 | */ 323 | console(message: string): Promise; 324 | 325 | /** 326 | * end: end a progress bar. 327 | * No need to call in most cases, unless you want to end before 100% 328 | */ 329 | end(): Promise; 330 | } 331 | ``` 332 | 333 | #### display 334 | 335 | What is displayed and display order, default: ':title :percent :bar :time 336 | :completed/:total' 337 | 338 | - `:title` progress bar title 339 | - `:percent` completion percentage 340 | - `:bar` the progress bar itself 341 | - `:time` time elapsed in seconds 342 | - `:eta` estimated completion time in seconds 343 | - `:completed` completed value 344 | - `:total` total number of ticks to complete 345 | 346 | ## Screenshots 347 | 348 | Standard use 349 | 350 | ![normal](./screenshots/title.gif) 351 | 352 | Multi-line progress bar output in terminal 353 | 354 | ![normal](./screenshots/multi.gif) 355 | 356 | Change how the order and look of elements 357 | 358 | ![console](./screenshots/display.gif) 359 | 360 | Change character color 361 | 362 | ![console](./screenshots/changeColor.gif) 363 | 364 | Change background color 365 | 366 | ![console](./screenshots/changeBgColor.gif) 367 | 368 | Color that changes with progress 369 | 370 | ![console](./screenshots/colorProgression.gif) 371 | 372 | Precise bar with more intermediate states 373 | 374 | ![console](./screenshots/preciseBar.gif) 375 | 376 | Wider bar 377 | 378 | ![console](./screenshots/width.gif) 379 | 380 | Clear the bar once finished 381 | 382 | ![clear](./screenshots/clear.gif) 383 | 384 | Backward progress 385 | 386 | ![backward](./screenshots/backward.gif) 387 | 388 | Log some messages 389 | 390 | ![console](./screenshots/console.gif) 391 | 392 | Log some messages next to the bar 393 | 394 | ![console](./screenshots/info.gif) 395 | 396 | More screenshots in the `screenshots` folder. 397 | 398 | ## Contributors 399 | 400 | 401 | 402 | 403 | 404 | Thanks for their contributions! 405 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### v1.5.1 - 2024.10.30 4 | 5 | Changed `Deno.stdout.writable.getWriter()` to `writeAll` in `@std/io/write-all`. 6 | 7 | Initially, I was using `writeAllSync`, but on December 27, 2023, I discovered that `writeAllSync` was deprecated, and the documentation recommended using the `stream API`. As a result, I released version 1.4.1 (Remove deprecated writeAllSync, Use WritableStream instead). However, both `writeAll` and `writeAllSync` are now available again, and I plan to switch back. Currently using the promise style, so I changed to use `writeAll`. 8 | 9 | ### v1.5.0 - 2024.10.30 10 | 11 | [Allow to configure the writer to something other than stdout](https://github.com/deno-library/progress/issues/30) 12 | 13 | ### v1.4.9 - 2024.04.02 14 | 15 | [Both JSR and HTTPS are supported](https://github.com/deno-library/progress/issues/29) 16 | 17 | ### v1.4.6 - 2024.03.27 18 | 19 | [support JSR](https://github.com/deno-library/progress/issues/28) 20 | 21 | ### v1.4.5 - 2024.01.26 22 | 23 | fix: [#26](https://github.com/deno-library/progress/issues/26) 24 | 25 | ### v1.4.4 - 2024.01.05 26 | 27 | fix: The stream is already locked 28 | 29 | ### v1.4.3 - 2024.01.04 30 | 31 | fix: The stream is already locked 32 | 33 | ### v1.4.2 - 2024.01.04 34 | 35 | remove `addSignalListener`: Deno Version 1.39.1, `deno test` no longer reports errors 36 | 37 | ### v1.4.1 - 2023.12.27 38 | 39 | Remove deprecated `writeAllSync`, Use `WritableStream` instead. 40 | 41 | ### v1.4.0 - 2023.11.12 42 | 43 | update to use [deno standard library v0.206.0](https://deno.land/std@0.206.0) 44 | 45 | ### v1.3.9 - 2023.08.31 46 | 47 | fixed [Incorrect bar size when color is used in the title](https://github.com/deno-library/progress/issues/24) 48 | 49 | ### v1.3.0 - 2022.11.7 50 | 51 | changes: 52 | 53 | 1. fixed [colored string length calculation bug](https://github.com/deno-library/progress/issues/8) 54 | 55 | 2. **Deno.consoleSize is now stable** 56 | 57 | > [Deno v1.27.0 : Stabilize Deno.consoleSize() API](https://github.com/denoland/deno/pull/15933) 58 | 59 | The Deno.consoleSize API change 60 | 61 | ```diff 62 | - Deno.consoleSize(Deno.stdout.rid).columns; 63 | + Deno.consoleSize().columns; 64 | ``` 65 | 66 | Now you can run a wider bar without unstable. 67 | 68 | ```diff 69 | - deno run --unstable ./examples/width.unstable.ts 70 | + deno run ./examples/width.ts 71 | ``` 72 | 73 | So `mod.unstable.ts` and `exmaples/width.unstable.ts` was removed. 74 | 75 | ```diff 76 | - mod.unstable.ts 77 | - exmaples/width.unstable.ts 78 | ``` 79 | 80 | ### v1.2.9 - 2022.11.7 81 | 82 | [Make this lib useable in deno tests](https://github.com/deno-library/progress/issues/13). 83 | 84 | ### v1.2.6 - 2022.5.30 85 | 86 | [Add option to show ETA](https://github.com/deno-library/progress/issues/9). 87 | 88 | ### v1.2.0 - 2020.12.5 89 | 90 | Add support for "Render multiple progress bars"\ 91 | [Thanks "shixiaobao17145" for the great idea](https://github.com/deno-library/progress/issues/7). 92 | 93 | ### v1.1.1 - 2020.07.15 94 | 95 | changes: add mod.unstable.ts and ./exmaples/width.unstable.ts 96 | 97 | > Deno v1.2.0 started to support tty column, but is unstable 98 | 99 | ```bash 100 | deno run --unstable ./examples/width.unstable.ts 101 | ``` 102 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deno-library/progress", 3 | "version": "1.5.1", 4 | "exports": { 5 | ".": "./mod.ts", 6 | "./time": "./time.ts" 7 | }, 8 | "lock": false, 9 | "publish": { 10 | "exclude": [ 11 | "examples", 12 | "screenshots", 13 | "!screenshots/logo.png", 14 | "tests", 15 | ".vscode", 16 | "changelog.md" 17 | ] 18 | }, 19 | "compilerOptions": {}, 20 | "fmt": { 21 | "exclude": ["README.md"] 22 | } 23 | } -------------------------------------------------------------------------------- /deps.example.ts: -------------------------------------------------------------------------------- 1 | export { delay } from "jsr:@std/async@0.221.0"; 2 | export * from "jsr:@std/fmt@0.221.0/colors"; 3 | export { 4 | simpleCallbackTarget, 5 | timerSource, 6 | } from "https://deno.land/x/rx_webstreams@0.2.0/mod.ts"; 7 | -------------------------------------------------------------------------------- /deps.test.ts: -------------------------------------------------------------------------------- 1 | export { assertEquals } from "jsr:@std/assert@0.221.0"; 2 | export { simpleTimerStream } from "https://deno.land/x/simple_timer_stream@1.0.2/mod.ts"; -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { bgGreen, bgWhite, stripAnsiCode } from "jsr:@std/fmt@1.0.3/colors"; 2 | export { writeAll } from "jsr:@std/io@0.225.0/write-all"; 3 | // export type { Writer } from "jsr:@std/io@0.225.0/types"; -------------------------------------------------------------------------------- /examples/backward.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const progress = new ProgressBar({ 5 | title: "backward", 6 | total: 100, 7 | }); 8 | 9 | let completed = 0; 10 | 11 | async function forward() { 12 | while (completed <= 60) { 13 | await progress.render(completed++); 14 | await delay(20); 15 | } 16 | await backward(); 17 | } 18 | 19 | async function backward() { 20 | while (completed > 0) { 21 | // ==> here 22 | await progress.render(--completed); 23 | // <== here 24 | await delay(20); 25 | } 26 | progress.end(); 27 | } 28 | 29 | forward(); 30 | -------------------------------------------------------------------------------- /examples/changeBgColor.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | import { bgCyan, bgMagenta } from "../deps.example.ts"; 4 | 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | total, 9 | }); 10 | 11 | let completed = 0; 12 | 13 | async function download() { 14 | while (completed <= total) { 15 | if (completed >= 20) { 16 | await progress.render(completed++, { 17 | // ==> here 18 | complete: bgMagenta(" "), 19 | incomplete: bgCyan(" "), 20 | // <== here 21 | }); 22 | } else { 23 | await progress.render(completed++); 24 | } 25 | 26 | await delay(20); 27 | } 28 | } 29 | 30 | await download(); 31 | -------------------------------------------------------------------------------- /examples/changeColor.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | import { green, yellow } from "../deps.example.ts"; 4 | 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | total, 9 | complete: "=", 10 | incomplete: "-", 11 | }); 12 | 13 | let completed = 0; 14 | 15 | async function download() { 16 | while (completed <= total) { 17 | if (completed >= 20) { 18 | await progress.render(completed++, { 19 | // ==> here 20 | complete: green("="), 21 | incomplete: yellow("-"), 22 | // <== here 23 | }); 24 | } else { 25 | await progress.render(completed++); 26 | } 27 | 28 | await delay(20); 29 | } 30 | } 31 | 32 | await download(); 33 | -------------------------------------------------------------------------------- /examples/clear.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const total = 100; 5 | 6 | const progress = new ProgressBar({ 7 | total, 8 | // ==> here 9 | clear: true, 10 | // <== here 11 | }); 12 | 13 | let completed = 0; 14 | 15 | async function download() { 16 | while (completed <= total) { 17 | await progress.render(completed++); 18 | 19 | await delay(20); 20 | } 21 | } 22 | 23 | await download(); 24 | -------------------------------------------------------------------------------- /examples/colorProgression.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { bgRgb24 } from "../deps.example.ts"; 3 | import { delay } from "../deps.example.ts"; 4 | 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | total, 9 | }); 10 | 11 | let completed = 0; 12 | 13 | async function download() { 14 | while (completed <= total) { 15 | await progress.render(completed++, { 16 | // ==> here 17 | complete: bgRgb24(" ", { r: 128, g: (completed / total) * 255, b: 0 }), 18 | // <== here 19 | }); 20 | 21 | await delay(20); 22 | } 23 | } 24 | 25 | await download(); 26 | -------------------------------------------------------------------------------- /examples/complete.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const total = 100; 5 | 6 | const progress = new ProgressBar({ 7 | total, 8 | // ==> here 9 | complete: "=", 10 | incomplete: "-", 11 | // <== here 12 | }); 13 | 14 | let completed = 0; 15 | 16 | async function download() { 17 | while (completed <= total) { 18 | progress.render(completed++); 19 | 20 | await delay(20); 21 | } 22 | } 23 | 24 | await download(); 25 | -------------------------------------------------------------------------------- /examples/console.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const title = "interval:"; 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | title, 9 | total, 10 | }); 11 | 12 | let completed = 0; 13 | 14 | async function download() { 15 | while (completed <= total) { 16 | await progress.render(completed++); 17 | 18 | // here ==> 19 | if (completed % 20 === 0) await progress.console(completed); 20 | // <== here 21 | 22 | await delay(50); 23 | } 24 | } 25 | 26 | await download(); 27 | -------------------------------------------------------------------------------- /examples/display.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const total = 100; 5 | 6 | const progress = new ProgressBar({ 7 | total, 8 | complete: "=", 9 | incomplete: "-", 10 | // here ==> 11 | // display: ':bar' 12 | // display: ':bar :time' 13 | // display: '[:bar]' 14 | // display: 'hello :bar world' 15 | display: ":completed/:total hello :time [:bar] :percent", 16 | // <== here 17 | }); 18 | 19 | let completed = 0; 20 | 21 | async function download() { 22 | while (completed <= total) { 23 | await progress.render(completed++); 24 | 25 | await delay(20); 26 | } 27 | } 28 | 29 | await download(); 30 | -------------------------------------------------------------------------------- /examples/eta.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const total = 100; 5 | 6 | const progress = new ProgressBar({ 7 | total, 8 | // here ==> 9 | // display: ":bar :eta", 10 | // display: ":bar :percent :time :eta", 11 | display: ":bar :percent elapsed :time eta :eta", 12 | // <== here 13 | }); 14 | 15 | let completed = 0; 16 | 17 | async function download() { 18 | while (completed <= total) { 19 | await progress.render(completed++); 20 | 21 | await delay(20); 22 | } 23 | } 24 | 25 | await download(); 26 | -------------------------------------------------------------------------------- /examples/info.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const total = 100; 5 | 6 | const progress = new ProgressBar({ 7 | total, 8 | // ==> here 9 | display: ":bar :title", 10 | // <== here 11 | }); 12 | 13 | let completed = 0; 14 | 15 | function* log() { 16 | yield "INFO: started"; 17 | yield "WARN "; 18 | yield "ERROR: X "; 19 | yield "custom text "; 20 | yield "end "; 21 | } 22 | 23 | const info = log(); 24 | 25 | async function download() { 26 | while (completed <= total) { 27 | await progress.render(completed++, { 28 | title: completed % 20 === 0 ? info.next().value + "" : "", 29 | }); 30 | 31 | await delay(100); 32 | } 33 | } 34 | 35 | await download(); 36 | -------------------------------------------------------------------------------- /examples/multi.ts: -------------------------------------------------------------------------------- 1 | import { MultiProgressBar } from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const title = "download files"; 5 | const total = 100; 6 | 7 | const bars = new MultiProgressBar({ 8 | title, 9 | // clear: true, 10 | complete: "=", 11 | incomplete: "-", 12 | display: "[:bar] :text :eta", 13 | }); 14 | 15 | let completed1 = 0; 16 | let completed2 = 0; 17 | 18 | async function download() { 19 | while (completed1 <= total || completed2 <= total) { 20 | completed1 += 1; 21 | completed2 += 2; 22 | await bars.render([ 23 | { 24 | completed: completed1, 25 | total, 26 | text: "file1", 27 | complete: "*", 28 | incomplete: ".", 29 | }, 30 | { completed: completed2, total, text: "file2" }, 31 | ]); 32 | 33 | await delay(50); 34 | } 35 | } 36 | 37 | await download(); 38 | -------------------------------------------------------------------------------- /examples/multi2.ts: -------------------------------------------------------------------------------- 1 | import { MultiProgressBar } from "../mod.ts"; 2 | import { 3 | simpleCallbackTarget, 4 | timerSource, 5 | } from "../deps.example.ts"; 6 | 7 | const bars = new MultiProgressBar({ title: "Downloading Files: " }); 8 | 9 | const timer1 = timerSource({ 10 | maxEventCount: 100, 11 | intervalInMilliseconds: 40, 12 | }); 13 | 14 | const timer2 = timerSource({ 15 | maxEventCount: 100, 16 | intervalInMilliseconds: 60, 17 | }); 18 | 19 | const timer3 = timerSource({ 20 | maxEventCount: 100, 21 | intervalInMilliseconds: 70, 22 | }); 23 | 24 | const progressArray = [ 25 | { completed: 0, total: 100, text: "Timer 1" }, 26 | { completed: 0, total: 100, text: "Timer 2" }, 27 | { completed: 0, total: 100, text: "Timer 3" }, 28 | ]; 29 | 30 | const renderProgress = () => { 31 | bars.render(progressArray); 32 | }; 33 | 34 | const promise1 = timer1.pipeTo( 35 | simpleCallbackTarget((progress) => { 36 | progressArray[0].completed = progress; 37 | renderProgress(); 38 | }), 39 | ); 40 | 41 | const promise2 = timer2.pipeTo( 42 | simpleCallbackTarget((progress) => { 43 | progressArray[1].completed = progress; 44 | renderProgress(); 45 | }), 46 | ); 47 | 48 | const promise3 = timer3.pipeTo( 49 | simpleCallbackTarget((progress) => { 50 | progressArray[2].completed = progress; 51 | renderProgress(); 52 | }), 53 | ); 54 | 55 | await Promise.all([promise1, promise2, promise3]); 56 | 57 | // Needed to not crush deno test (probably due to timer3 not running correctly for some reason) 58 | // bars.end(); 59 | -------------------------------------------------------------------------------- /examples/multiPrettyTime.ts: -------------------------------------------------------------------------------- 1 | import { MultiProgressBar } from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const title = "download files"; 5 | const total = 100; 6 | 7 | const bars = new MultiProgressBar({ 8 | title, 9 | // clear: true, 10 | complete: "=", 11 | incomplete: "-", 12 | display: "[:bar] :text time: :time tea: :eta", 13 | prettyTime: true, 14 | }); 15 | 16 | let completed1 = 0; 17 | let completed2 = 0; 18 | 19 | async function download() { 20 | if (completed1 <= total || completed2 <= total) { 21 | completed1 += 1; 22 | completed2 += 2; 23 | await bars.render([ 24 | { 25 | completed: completed1, 26 | total, 27 | text: "file1", 28 | complete: "*", 29 | incomplete: ".", 30 | prettyTimeOptions: { 31 | withSpaces: true, 32 | toFixedVal: 0, 33 | longFormat: true, 34 | }, 35 | }, 36 | { 37 | completed: completed2, 38 | total, 39 | text: "file2", 40 | prettyTimeOptions: { 41 | withSpaces: false, 42 | toFixedVal: 2, 43 | longFormat: false, 44 | }, 45 | }, 46 | ]); 47 | 48 | await delay(1000); 49 | } 50 | } 51 | 52 | await download(); 53 | -------------------------------------------------------------------------------- /examples/preciseBar.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { bgWhite, green } from "../deps.example.ts"; 3 | import { delay } from "../deps.example.ts"; 4 | 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | total, 9 | // Note: on Windows, if UTF-8 is not the default encoding for the terminal, such characters will not be displayed as expected. 10 | // ==> here 11 | preciseBar: [ 12 | bgWhite(green("▏")), 13 | bgWhite(green("▎")), 14 | bgWhite(green("▍")), 15 | bgWhite(green("▌")), 16 | bgWhite(green("▋")), 17 | bgWhite(green("▊")), 18 | bgWhite(green("▉")), 19 | ], 20 | // <== here 21 | }); 22 | 23 | let completed = 0; 24 | 25 | async function download() { 26 | while (completed <= total) { 27 | await progress.render(completed++); 28 | 29 | await delay(50); 30 | } 31 | } 32 | 33 | await download(); 34 | -------------------------------------------------------------------------------- /examples/prettyTime.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const total = 100; 5 | 6 | const progress = new ProgressBar({ 7 | total, 8 | complete: "=", 9 | incomplete: "-", 10 | display: ":bar :percent elapsed :time eta :eta", 11 | // here ==> 12 | prettyTime: true, 13 | // <== here 14 | }); 15 | 16 | let completed = 0; 17 | 18 | async function download() { 19 | while (completed <= total) { 20 | await progress.render(completed++, { 21 | prettyTimeOptions: { 22 | withSpaces: true, 23 | toFixedVal: 0, 24 | longFormat: true, 25 | }, 26 | }); 27 | 28 | await delay(1000); 29 | } 30 | } 31 | 32 | await download(); 33 | -------------------------------------------------------------------------------- /examples/stderr.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const total = 100; 5 | 6 | const progress = new ProgressBar({ 7 | total, 8 | // ==> here 9 | output: Deno.stderr, 10 | // <== here 11 | }); 12 | 13 | let completed = 0; 14 | 15 | async function download() { 16 | while (completed <= total) { 17 | await progress.render(completed++); 18 | 19 | await delay(50); 20 | } 21 | } 22 | 23 | await download(); 24 | -------------------------------------------------------------------------------- /examples/title.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const title = "progress:"; 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | // here ==> 9 | title, 10 | // <== here 11 | total, 12 | }); 13 | 14 | let completed = 0; 15 | 16 | async function download() { 17 | while (completed <= total) { 18 | await progress.render(completed++); 19 | 20 | await delay(50); 21 | } 22 | } 23 | 24 | await download(); 25 | -------------------------------------------------------------------------------- /examples/total.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const title = "total test"; 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | title, 9 | // Can also be set within the render method 10 | // total 11 | }); 12 | 13 | let completed = 0; 14 | 15 | async function download() { 16 | while (completed <= total) { 17 | // Can also be set in the constructor 18 | // ==> here 19 | await progress.render(completed++, { total }); 20 | // <== here 21 | 22 | await delay(20); 23 | } 24 | } 25 | 26 | await download(); 27 | -------------------------------------------------------------------------------- /examples/width.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | const title = "interval:"; 5 | const total = 100; 6 | 7 | const progress = new ProgressBar({ 8 | title, 9 | total, 10 | // here ==> 11 | // width: 20 12 | width: 1000, // longer than the terminal width 13 | // <== here 14 | }); 15 | 16 | let completed = 0; 17 | 18 | async function download() { 19 | while (completed <= total) { 20 | await progress.render(completed++); 21 | 22 | await delay(20); 23 | } 24 | } 25 | 26 | await download(); 27 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { bgGreen, bgWhite, stripAnsiCode } from "./deps.ts"; 2 | import { writeAll } from "./deps.ts"; 3 | // import type { Writer } from "./deps.ts"; 4 | import { prettyTime, type prettyTimeOptions } from "./time.ts"; 5 | export { MultiProgressBar } from "./multi.ts"; 6 | 7 | const isTerminal = Deno.stdout.isTerminal; 8 | 9 | const enum Direction { 10 | left, 11 | right, 12 | all, 13 | } 14 | 15 | interface constructorOptions { 16 | title?: string; 17 | total?: number; 18 | width?: number; 19 | complete?: string; 20 | preciseBar?: string[]; 21 | incomplete?: string; 22 | clear?: boolean; 23 | interval?: number; 24 | display?: string; 25 | prettyTime?: boolean; 26 | output?: typeof Deno.stdout | typeof Deno.stderr; 27 | } 28 | 29 | interface renderOptions { 30 | title?: string; 31 | total?: number; 32 | text?: string; 33 | complete?: string; 34 | preciseBar?: string[]; 35 | incomplete?: string; 36 | prettyTimeOptions?: prettyTimeOptions; 37 | } 38 | 39 | /** 40 | * ProgressBar single progress bar. 41 | */ 42 | export default class ProgressBar { 43 | title: string; 44 | total?: number; 45 | width: number; 46 | complete: string; 47 | preciseBar: string[]; 48 | incomplete: string; 49 | clear: boolean; 50 | interval: number; 51 | display: string; 52 | prettyTime: boolean; 53 | 54 | #end = false; 55 | private lastStr = ""; 56 | private lastStrLen = 0; 57 | private start = Date.now(); 58 | private lastRenderTime = 0; 59 | private encoder = new TextEncoder(); 60 | // private writer: WritableStreamDefaultWriter; 61 | // private writer: Writer; 62 | private writer: typeof Deno.stdout | typeof Deno.stderr; 63 | 64 | /** 65 | * Title, total, complete, incomplete, can also be set or changed in the render method 66 | * 67 | * - title Progress bar title, default: '' 68 | * - total total number of ticks to complete, 69 | * - width the displayed width of the progress, default: 50 70 | * - complete completion character, default: colors.bgGreen(' '), can use any string 71 | * - incomplete incomplete character, default: colors.bgWhite(' '), can use any string 72 | * - clear clear the bar on completion, default: false 73 | * - interval minimum time between updates in milliseconds, default: 16 74 | * - display What is displayed and display order, default: ':title :percent :bar :time :completed/:total' 75 | * - prettyTime Whether to pretty print time and eta 76 | * - output Output stream, can be Deno.stdout or Deno.stderr, default is Deno.stdout 77 | */ 78 | constructor({ 79 | title = "", 80 | total, 81 | width = 50, 82 | complete = bgGreen(" "), 83 | preciseBar = [], 84 | incomplete = bgWhite(" "), 85 | clear = false, 86 | interval = 16, 87 | display, 88 | prettyTime = false, 89 | output = Deno.stdout, 90 | }: constructorOptions = {}) { 91 | this.title = title; 92 | this.total = total; 93 | this.width = width; 94 | this.complete = complete; 95 | this.preciseBar = preciseBar.concat(complete); 96 | this.incomplete = incomplete; 97 | this.clear = clear; 98 | this.interval = interval; 99 | this.display = display ?? 100 | ":title :percent :bar :time :completed/:total :text"; 101 | this.prettyTime = prettyTime; 102 | // this.writer = output.writable.getWriter(); 103 | this.writer = output; 104 | } 105 | 106 | /** 107 | * "render" the progress bar 108 | * 109 | * - `completed` completed value 110 | * - `options` optional parameters 111 | * - `title` progress bar title 112 | * - `total` total number of ticks to complete 113 | * - `text` optional, custom text, default: '' 114 | * - `complete` completion character, If you want to change at a certain moment. For example, it turns red at 20% 115 | * - `incomplete` incomplete character, If you want to change at a certain moment. For example, it turns red at 20% 116 | * - `prettyTimeOptions` prettyTime options 117 | */ 118 | async render(completed: number, options: renderOptions = {}): Promise { 119 | if (this.#end || !isTerminal) return; 120 | 121 | if (completed < 0) { 122 | throw new Error(`completed must greater than or equal to 0`); 123 | } 124 | 125 | const total = options.total ?? this.total ?? 100; 126 | const now = Date.now(); 127 | const ms = now - this.lastRenderTime; 128 | const end = completed >= total; 129 | if (ms < this.interval && !end) return; 130 | 131 | this.lastRenderTime = now; 132 | const time = this.prettyTime 133 | ? prettyTime(now - this.start, options.prettyTimeOptions) 134 | : ((now - this.start) / 1000).toFixed(1) + "s"; 135 | const msEta = completed >= total 136 | ? 0 137 | : (total / completed - 1) * (now - this.start); 138 | const eta = completed == 0 139 | ? "-" 140 | : this.prettyTime 141 | ? prettyTime(msEta, options.prettyTimeOptions) 142 | : (msEta / 1000).toFixed(1) + "s"; 143 | 144 | const percent = ((completed / total) * 100).toFixed(2) + "%"; 145 | 146 | // :title :percent :bar :time :completed/:total 147 | let str = this.display 148 | .replace(":title", options.title ?? this.title) 149 | .replace(":time", time) 150 | .replace(":text", options.text ?? "") 151 | .replace(":eta", eta) 152 | .replace(":percent", percent) 153 | .replace(":completed", completed + "") 154 | .replace(":total", total + ""); 155 | 156 | // compute the available space (non-zero) for the bar 157 | const availableSpace = Math.max( 158 | 0, 159 | this.ttyColumns - stripAnsiCode(str.replace(":bar", "")).length, 160 | ); 161 | 162 | const width = Math.min(this.width, availableSpace); 163 | 164 | const preciseBar = options.preciseBar ?? this.preciseBar; 165 | const precision = preciseBar.length > 1; 166 | 167 | // :bar 168 | const completeLength = (width * completed) / total; 169 | const roundedCompleteLength = Math.floor(completeLength); 170 | 171 | let precise = ""; 172 | if (precision) { 173 | const preciseLength = completeLength - roundedCompleteLength; 174 | precise = end 175 | ? "" 176 | : preciseBar[Math.floor(preciseBar.length * preciseLength)]; 177 | } 178 | 179 | const complete = new Array(roundedCompleteLength) 180 | .fill(options.complete ?? this.complete) 181 | .join(""); 182 | const incomplete = new Array( 183 | Math.max(width - roundedCompleteLength - (precision ? 1 : 0), 0), 184 | ) 185 | .fill(options.incomplete ?? this.incomplete) 186 | .join(""); 187 | 188 | str = str.replace(":bar", complete + precise + incomplete); 189 | 190 | if (str !== this.lastStr) { 191 | const strLen = stripAnsiCode(str).length; 192 | if (strLen < this.lastStrLen) { 193 | str += " ".repeat(this.lastStrLen - strLen); 194 | } 195 | await this.write(str); 196 | this.lastStr = str; 197 | this.lastStrLen = strLen; 198 | } 199 | 200 | if (end) await this.end(); 201 | } 202 | 203 | /** 204 | * end: end a progress bar. 205 | * No need to call in most cases, unless you want to end before 100% 206 | */ 207 | async end(): Promise { 208 | if (this.#end) return; 209 | this.#end = true; 210 | if (this.clear) { 211 | await this.stdoutWrite("\r"); 212 | await this.clearLine(); 213 | } else { 214 | await this.breakLine(); 215 | } 216 | await this.showCursor(); 217 | // this.writer.releaseLock(); 218 | } 219 | 220 | /** 221 | * interrupt the progress bar and write a message above it 222 | * 223 | * @param message The message to write 224 | */ 225 | async console(message: string | number): Promise { 226 | await this.clearLine(); 227 | await this.write(`${message}`); 228 | await this.breakLine(); 229 | await this.write(this.lastStr); 230 | } 231 | 232 | private write(msg: string): Promise { 233 | return this.stdoutWrite(`\r${msg}\x1b[?25l`); 234 | } 235 | 236 | private get ttyColumns(): number { 237 | if (!Deno.stdout.isTerminal()) return 100; 238 | return Deno.consoleSize().columns; 239 | } 240 | 241 | private breakLine(): Promise { 242 | return this.stdoutWrite("\n"); 243 | } 244 | 245 | private stdoutWrite(msg: string): Promise { 246 | // return this.writer.write(this.encoder.encode(msg)); 247 | return writeAll(this.writer, this.encoder.encode(msg)) 248 | } 249 | 250 | private clearLine(direction: Direction = Direction.all): Promise { 251 | switch (direction) { 252 | case Direction.all: 253 | return this.stdoutWrite("\x1b[2K"); 254 | case Direction.left: 255 | return this.stdoutWrite("\x1b[1K"); 256 | case Direction.right: 257 | return this.stdoutWrite("\x1b[0K"); 258 | } 259 | } 260 | 261 | private showCursor(): Promise { 262 | return this.stdoutWrite("\x1b[?25h"); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /multi.ts: -------------------------------------------------------------------------------- 1 | import { bgGreen, bgWhite, stripAnsiCode } from "./deps.ts"; 2 | import { writeAll } from "./deps.ts"; 3 | // import type { Writer } from "./deps.ts"; 4 | import { prettyTime, type prettyTimeOptions } from "./time.ts"; 5 | 6 | const isTerminal = Deno.stdout.isTerminal; 7 | 8 | interface constructorOptions { 9 | title?: string; 10 | width?: number; 11 | complete?: string; 12 | incomplete?: string; 13 | clear?: boolean; 14 | interval?: number; 15 | display?: string; 16 | prettyTime?: boolean; 17 | output?: typeof Deno.stdout | typeof Deno.stderr; 18 | } 19 | 20 | interface renderOptions { 21 | completed: number; 22 | text?: string; 23 | total?: number; 24 | complete?: string; 25 | incomplete?: string; 26 | prettyTimeOptions?: prettyTimeOptions; 27 | } 28 | 29 | interface bar { 30 | str: string; 31 | strLen?: number; 32 | end?: boolean; 33 | } 34 | 35 | /** 36 | * MultiProgressBar multiple progress bars. 37 | */ 38 | export class MultiProgressBar { 39 | width: number; 40 | complete: string; 41 | incomplete: string; 42 | clear: boolean; 43 | interval: number; 44 | display: string; 45 | prettyTime: boolean; 46 | 47 | #end = false; 48 | #startIndex = 0; 49 | #lastRows = 0; 50 | #bars: bar[] = []; 51 | private lastStr = ""; 52 | private start = Date.now(); 53 | private lastRenderTime = 0; 54 | private encoder = new TextEncoder(); 55 | // private writer: WritableStreamDefaultWriter; 56 | // private writer: Writer; 57 | private writer: typeof Deno.stdout | typeof Deno.stderr; 58 | 59 | /** 60 | * Title, total, complete, incomplete, can also be set or changed in the render method 61 | * 62 | * - title Progress bar title, default: '' 63 | * - width the displayed width of the progress, default: 50 64 | * - complete completion character, default: colors.bgGreen(' '), can use any string 65 | * - incomplete incomplete character, default: colors.bgWhite(' '), can use any string 66 | * - clear clear the bar on completion, default: false 67 | * - interval minimum time between updates in milliseconds, default: 16 68 | * - display What is displayed and display order, default: ':bar :text :percent :time :completed/:total' 69 | * - prettyTime Whether to pretty print time and eta 70 | * - output Output stream, can be Deno.stdout or Deno.stderr, default is Deno.stdout 71 | */ 72 | constructor({ 73 | title = "", 74 | width = 50, 75 | complete = bgGreen(" "), 76 | incomplete = bgWhite(" "), 77 | clear = false, 78 | interval, 79 | display, 80 | prettyTime = false, 81 | output = Deno.stdout, 82 | }: constructorOptions = {}) { 83 | if (title != "") { 84 | this.#bars.push({ str: title }); 85 | this.#startIndex = 1; 86 | } 87 | this.width = width; 88 | this.complete = complete; 89 | this.incomplete = incomplete; 90 | this.clear = clear; 91 | this.interval = interval ?? 16; 92 | this.display = display ?? ":bar :text :percent :time :completed/:total"; 93 | this.prettyTime = prettyTime; 94 | // this.writer = output.writable.getWriter(); 95 | this.writer = output; 96 | } 97 | 98 | /** 99 | * "render" the progress bar 100 | * 101 | * - `bars` progress bars 102 | * - `completed` completed value 103 | * - `total` optional, total number of ticks to complete, default: 100 104 | * - `text` optional, text displayed per ProgressBar, default: '' 105 | * - `complete` - optional, completion character 106 | * - `incomplete` - optional, incomplete character 107 | * - `prettyTimeOptions` - prettyTime options 108 | */ 109 | async render(bars: Array): Promise { 110 | if (this.#end || !isTerminal) return; 111 | 112 | const now = Date.now(); 113 | const ms = now - this.lastRenderTime; 114 | this.lastRenderTime = now; 115 | let end = true; 116 | let index = this.#startIndex; 117 | 118 | for (const { completed, total = 100, text = "", ...options } of bars) { 119 | if (completed < 0) { 120 | throw new Error(`completed must greater than or equal to 0`); 121 | } 122 | if (!Number.isInteger(total)) throw new Error(`total must be 'number'`); 123 | if (this.#bars[index] && this.#bars[index].end) { 124 | index++; 125 | continue; 126 | } 127 | end = false; 128 | const percent = ((completed / total) * 100).toFixed(2) + "%"; 129 | const time = this.prettyTime 130 | ? prettyTime(now - this.start, options.prettyTimeOptions) 131 | : ((now - this.start) / 1000).toFixed(1) + "s"; 132 | const msEta = completed >= total 133 | ? 0 134 | : (total / completed - 1) * (now - this.start); 135 | const eta = completed == 0 136 | ? "-" 137 | : this.prettyTime 138 | ? prettyTime(msEta, options.prettyTimeOptions) 139 | : (msEta / 1000).toFixed(1) + "s"; 140 | 141 | // :bar :text :percent :time :completed/:total 142 | let str = this.display 143 | .replace(":text", text) 144 | .replace(":time", time) 145 | .replace(":eta", eta) 146 | .replace(":percent", percent) 147 | .replace(":completed", completed + "") 148 | .replace(":total", total + ""); 149 | 150 | // compute the available space (non-zero) for the bar 151 | const availableSpace = Math.max( 152 | 0, 153 | this.ttyColumns - stripAnsiCode(str.replace(":bar", "")).length, 154 | ); 155 | 156 | const width = Math.min(this.width, availableSpace); 157 | // :bar 158 | const completeLength = Math.round((width * completed) / total); 159 | const complete = new Array(completeLength) 160 | .fill(options.complete ?? this.complete) 161 | .join(""); 162 | const incomplete = new Array(width - completeLength) 163 | .fill(options.incomplete ?? this.incomplete) 164 | .join(""); 165 | 166 | str = str.replace(":bar", complete + incomplete); 167 | const strLen = stripAnsiCode(str).length; 168 | if (this.#bars[index] && str != this.#bars[index].str) { 169 | const lastStrLen = this.#bars[index].strLen!; 170 | if (strLen < lastStrLen) { 171 | str += " ".repeat(lastStrLen - strLen); 172 | } 173 | } 174 | 175 | this.#bars[index++] = { 176 | str, 177 | strLen, 178 | end: completed >= total, 179 | }; 180 | } 181 | if (ms < this.interval && !end) return; 182 | const str = this.#bars.map((v) => v.str).join("\n"); 183 | 184 | if (str !== this.lastStr) { 185 | await this.resetScreen(); 186 | await this.write(str); 187 | this.lastStr = str; 188 | this.#lastRows = this.#bars.length; 189 | } 190 | 191 | if (end) await this.end(); 192 | } 193 | 194 | /** 195 | * end: end a progress bar. 196 | * No need to call in most cases, unless you want to end before 100% 197 | */ 198 | async end(): Promise { 199 | if (this.#end) return; 200 | this.#end = true; 201 | if (this.clear) { 202 | await this.resetScreen(); 203 | } else { 204 | await this.breakLine(); 205 | } 206 | await this.showCursor(); 207 | // this.writer.releaseLock(); 208 | } 209 | 210 | /** 211 | * interrupt the progress bar and write a message above it 212 | * 213 | * @param message The message to write 214 | */ 215 | async console(message: string | number): Promise { 216 | await this.resetScreen(); 217 | await this.write(`${message}`); 218 | await this.breakLine(); 219 | await this.write(this.lastStr); 220 | } 221 | 222 | private write(msg: string): Promise { 223 | return this.stdoutWrite(`${msg}\x1b[?25l`); 224 | } 225 | 226 | private async resetScreen(): Promise { 227 | if (this.#lastRows > 0) { 228 | await this.stdoutWrite("\x1b[" + (this.#lastRows - 1) + "A\r\x1b[?0J"); 229 | } 230 | } 231 | 232 | private get ttyColumns(): number { 233 | if (!Deno.stdout.isTerminal()) return 100; 234 | return Deno.consoleSize().columns; 235 | } 236 | 237 | private breakLine(): Promise { 238 | return this.stdoutWrite("\n"); 239 | } 240 | 241 | private stdoutWrite(msg: string): Promise { 242 | // return this.writer.write(this.encoder.encode(msg)); 243 | return writeAll(this.writer, this.encoder.encode(msg)) 244 | } 245 | 246 | private showCursor(): Promise { 247 | return this.stdoutWrite("\x1b[?25h"); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /screenshots/backward.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/backward.gif -------------------------------------------------------------------------------- /screenshots/changeBgColor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/changeBgColor.gif -------------------------------------------------------------------------------- /screenshots/changeColor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/changeColor.gif -------------------------------------------------------------------------------- /screenshots/clear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/clear.gif -------------------------------------------------------------------------------- /screenshots/colorProgression.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/colorProgression.gif -------------------------------------------------------------------------------- /screenshots/complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/complete.gif -------------------------------------------------------------------------------- /screenshots/console.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/console.gif -------------------------------------------------------------------------------- /screenshots/display.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/display.gif -------------------------------------------------------------------------------- /screenshots/info.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/info.gif -------------------------------------------------------------------------------- /screenshots/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/logo.png -------------------------------------------------------------------------------- /screenshots/multi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/multi.gif -------------------------------------------------------------------------------- /screenshots/preciseBar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/preciseBar.gif -------------------------------------------------------------------------------- /screenshots/title.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/title.gif -------------------------------------------------------------------------------- /screenshots/total.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/total.gif -------------------------------------------------------------------------------- /screenshots/width.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-library/progress/d021c59229583c6663486f015b4b31df4703bd3d/screenshots/width.gif -------------------------------------------------------------------------------- /tests/mod.test.ts: -------------------------------------------------------------------------------- 1 | import ProgressBar from "../mod.ts"; 2 | import { simpleTimerStream } from "../deps.test.ts"; 3 | 4 | Deno.test(`Use ProgressBar in a deno test`, async () => { 5 | const progress = new ProgressBar({ title: "downloading: ", total: 50 }); 6 | 7 | const timer = simpleTimerStream({ 8 | maxEventCount: 100, 9 | intervalInMilliseconds: 50, 10 | }); 11 | 12 | for await (const event of timer) { 13 | progress.render(event); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /tests/multi.test.ts: -------------------------------------------------------------------------------- 1 | import { MultiProgressBar } from "../mod.ts"; 2 | import { delay } from "../deps.example.ts"; 3 | 4 | Deno.test(`Use MultiProgressBar in a deno test`, async () => { 5 | const title = "download files"; 6 | const total = 100; 7 | 8 | const bars = new MultiProgressBar({ 9 | title, 10 | // clear: true, 11 | complete: "=", 12 | incomplete: "-", 13 | display: "[:bar] :text :eta", 14 | }); 15 | 16 | let completed1 = 0; 17 | let completed2 = 0; 18 | 19 | while (completed1 <= total || completed2 <= total) { 20 | completed1 += 1; 21 | completed2 += 2; 22 | bars.render([ 23 | { 24 | completed: completed1, 25 | total, 26 | text: "file1", 27 | complete: "*", 28 | incomplete: ".", 29 | }, 30 | { completed: completed2, total, text: "file2" }, 31 | ]); 32 | 33 | await delay(100); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /tests/time.test.ts: -------------------------------------------------------------------------------- 1 | import { prettyTime } from "../time.ts"; 2 | import { assertEquals } from "../deps.test.ts"; 3 | 4 | Deno.test(`test prettyTime: default`, () => { 5 | // assertEquals(prettyTime(10), "10.0s"); 6 | const base = 111; 7 | assertEquals(prettyTime(base), "0.1s"); 8 | assertEquals(prettyTime(10 * base), "1.1s"); 9 | assertEquals(prettyTime(100 * base), "11.1s"); 10 | assertEquals(prettyTime(1000 * base), "1m51.0s"); 11 | assertEquals(prettyTime(10000 * base), "18m30.0s"); 12 | assertEquals(prettyTime(100000 * base), "3h5m0.0s"); 13 | assertEquals(prettyTime(1000000 * base), "1d6h50m0.0s"); 14 | }); 15 | 16 | Deno.test(`test prettyTime: withSpaces`, () => { 17 | // assertEquals(prettyTime(10), "10.0s"); 18 | const base = 111; 19 | assertEquals(prettyTime(base, { withSpaces: true }), "0.1s"); 20 | assertEquals(prettyTime(10 * base, { withSpaces: true }), "1.1s"); 21 | assertEquals(prettyTime(100 * base, { withSpaces: true }), "11.1s"); 22 | assertEquals(prettyTime(1000 * base, { withSpaces: true }), "1m 51.0s"); 23 | assertEquals(prettyTime(10000 * base, { withSpaces: true }), "18m 30.0s"); 24 | assertEquals(prettyTime(100000 * base, { withSpaces: true }), "3h 5m 0.0s"); 25 | assertEquals( 26 | prettyTime(1000000 * base, { withSpaces: true }), 27 | "1d 6h 50m 0.0s", 28 | ); 29 | }); 30 | 31 | Deno.test(`test prettyTime: toFixedVal`, () => { 32 | // assertEquals(prettyTime(10), "10.0s"); 33 | const base = 111; 34 | assertEquals(prettyTime(base, { toFixedVal: 0 }), "0s"); 35 | assertEquals(prettyTime(10 * base, { toFixedVal: 0 }), "1s"); 36 | assertEquals(prettyTime(100 * base, { toFixedVal: 0 }), "11s"); 37 | assertEquals(prettyTime(1000 * base, { toFixedVal: 0 }), "1m51s"); 38 | assertEquals(prettyTime(10000 * base, { toFixedVal: 0 }), "18m30s"); 39 | assertEquals(prettyTime(100000 * base, { toFixedVal: 0 }), "3h5m0s"); 40 | assertEquals(prettyTime(1000000 * base, { toFixedVal: 0 }), "1d6h50m0s"); 41 | }); 42 | 43 | Deno.test(`test prettyTime: longFormat`, () => { 44 | // assertEquals(prettyTime(10), "10.0s"); 45 | const base = 111; 46 | assertEquals(prettyTime(base, { longFormat: true }), "0.1second"); 47 | assertEquals(prettyTime(10 * base, { longFormat: true }), "1.1seconds"); 48 | assertEquals(prettyTime(100 * base, { longFormat: true }), "11.1seconds"); 49 | assertEquals( 50 | prettyTime(1000 * base, { longFormat: true }), 51 | "1minute51.0seconds", 52 | ); 53 | assertEquals( 54 | prettyTime(10000 * base, { longFormat: true }), 55 | "18minutes30.0seconds", 56 | ); 57 | assertEquals( 58 | prettyTime(100000 * base, { longFormat: true }), 59 | "3hours5minutes0.0second", 60 | ); 61 | assertEquals( 62 | prettyTime(1000000 * base, { longFormat: true }), 63 | "1day6hours50minutes0.0second", 64 | ); 65 | assertEquals( 66 | prettyTime(10000000 * base, { longFormat: true }), 67 | "12days20hours20minutes0.0second", 68 | ); 69 | }); 70 | 71 | Deno.test(`test prettyTime: withSpaces and toFixedVal`, () => { 72 | // assertEquals(prettyTime(10), "10.0s"); 73 | const base = 111; 74 | assertEquals( 75 | prettyTime(base, { withSpaces: true, toFixedVal: 0 }), 76 | "0s", 77 | ); 78 | assertEquals( 79 | prettyTime(10 * base, { withSpaces: true, toFixedVal: 0 }), 80 | "1s", 81 | ); 82 | assertEquals( 83 | prettyTime(100 * base, { withSpaces: true, toFixedVal: 0 }), 84 | "11s", 85 | ); 86 | assertEquals( 87 | prettyTime(1000 * base, { 88 | withSpaces: true, 89 | toFixedVal: 0, 90 | }), 91 | "1m 51s", 92 | ); 93 | assertEquals( 94 | prettyTime(10000 * base, { 95 | withSpaces: true, 96 | toFixedVal: 0, 97 | }), 98 | "18m 30s", 99 | ); 100 | assertEquals( 101 | prettyTime(100000 * base, { 102 | withSpaces: true, 103 | toFixedVal: 0, 104 | }), 105 | "3h 5m 0s", 106 | ); 107 | assertEquals( 108 | prettyTime(1000000 * base, { 109 | withSpaces: true, 110 | toFixedVal: 0, 111 | }), 112 | "1d 6h 50m 0s", 113 | ); 114 | assertEquals( 115 | prettyTime(10000000 * base, { 116 | withSpaces: true, 117 | toFixedVal: 0, 118 | }), 119 | "12d 20h 20m 0s", 120 | ); 121 | }); 122 | 123 | Deno.test(`test prettyTime: withSpaces and toFixedVal and longFormat`, () => { 124 | // assertEquals(prettyTime(10), "10.0s"); 125 | const base = 111; 126 | assertEquals( 127 | prettyTime(base, { 128 | longFormat: true, 129 | withSpaces: true, 130 | toFixedVal: 0, 131 | }), 132 | "0second", 133 | ); 134 | assertEquals( 135 | prettyTime(10 * base, { 136 | longFormat: true, 137 | withSpaces: true, 138 | toFixedVal: 0, 139 | }), 140 | "1second", 141 | ); 142 | assertEquals( 143 | prettyTime(100 * base, { 144 | longFormat: true, 145 | withSpaces: true, 146 | toFixedVal: 0, 147 | }), 148 | "11seconds", 149 | ); 150 | assertEquals( 151 | prettyTime(1000 * base, { 152 | longFormat: true, 153 | withSpaces: true, 154 | toFixedVal: 0, 155 | }), 156 | "1minute 51seconds", 157 | ); 158 | assertEquals( 159 | prettyTime(10000 * base, { 160 | longFormat: true, 161 | withSpaces: true, 162 | toFixedVal: 0, 163 | }), 164 | "18minutes 30seconds", 165 | ); 166 | assertEquals( 167 | prettyTime(100000 * base, { 168 | longFormat: true, 169 | withSpaces: true, 170 | toFixedVal: 0, 171 | }), 172 | "3hours 5minutes 0second", 173 | ); 174 | assertEquals( 175 | prettyTime(1000000 * base, { 176 | longFormat: true, 177 | withSpaces: true, 178 | toFixedVal: 0, 179 | }), 180 | "1day 6hours 50minutes 0second", 181 | ); 182 | assertEquals( 183 | prettyTime(10000000 * base, { 184 | longFormat: true, 185 | withSpaces: true, 186 | toFixedVal: 0, 187 | }), 188 | "12days 20hours 20minutes 0second", 189 | ); 190 | }); 191 | -------------------------------------------------------------------------------- /time.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * prettyTime options 3 | * - withSpaces Whether to use spaces to separate times, `1d2h3m5s` or `1d 2h 3m 5s`, default false 4 | * - toFixedVal value pass to toFixed for seconds, default 1 5 | * - longFormat Whether to use a long format, default false, `1d2h3m5s` or `1days 2hours 3minutes 5seconds` 6 | */ 7 | export interface prettyTimeOptions { 8 | withSpaces?: boolean; 9 | toFixedVal?: number; 10 | longFormat?: boolean; 11 | } 12 | 13 | /** 14 | * Convert time duration to a human readable string: 5d1h20m30s 15 | * 16 | * - milliseconds The number to format, unit milliseconds 17 | */ 18 | export function prettyTime( 19 | milliseconds: number, 20 | options: prettyTimeOptions = { 21 | withSpaces: false, 22 | toFixedVal: 1, 23 | longFormat: false, 24 | }, 25 | ): string { 26 | let second = milliseconds / 1000; 27 | if (second < 60) { 28 | return unitToString(second, 0, options); 29 | } 30 | let minute = Math.floor(second / 60); 31 | second %= 60; 32 | if (minute < 60) { 33 | return unitToString(minute, 1, options) + unitToString(second, 0, options); 34 | } 35 | let hour = Math.floor(minute / 60); 36 | minute %= 60; 37 | if (hour < 24) { 38 | return ( 39 | unitToString(hour, 2, options) + 40 | unitToString(minute, 1, options) + 41 | unitToString(second, 0, options) 42 | ); 43 | } 44 | const day = Math.floor(hour / 24); 45 | hour %= 24; 46 | return ( 47 | unitToString(day, 3, options) + 48 | unitToString(hour, 2, options) + 49 | unitToString(minute, 1, options) + 50 | unitToString(second, 0, options) 51 | ); 52 | } 53 | 54 | function unitToString( 55 | val: number, 56 | i: number, 57 | { withSpaces = false, toFixedVal = 1, longFormat = false }: prettyTimeOptions, 58 | ): string { 59 | const units = longFormat 60 | ? ["second", "minute", "hour", "day"] 61 | : ["s", "m", "h", "d"]; 62 | const unit = longFormat && (val >= 2 || (val > 1 && toFixedVal > 0)) 63 | ? units[i] + "s" 64 | : units[i]; 65 | if (i == 0) { 66 | return val.toFixed(toFixedVal) + unit; 67 | } 68 | return val + (withSpaces ? unit + " " : unit); 69 | } 70 | --------------------------------------------------------------------------------