├── .npmignore ├── tsconfig.json ├── example ├── emojis.js ├── colors.js ├── example.ts ├── example.js ├── spinner.svg └── screenshot.svg ├── package.json ├── LICENSE ├── .gitignore ├── dist ├── AsciiBar.d.ts └── AsciiBar.js ├── README.md └── src └── AsciiBar.ts /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | assets -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "declaration": true, 7 | "outDir": "dist", 8 | "esModuleInterop": true 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "dist", 13 | "example" 14 | ] 15 | } -------------------------------------------------------------------------------- /example/emojis.js: -------------------------------------------------------------------------------- 1 | const AsciiBar = require('ascii-bar').default; 2 | 3 | const TOTAL = 10; 4 | 5 | const bar = new AsciiBar({ 6 | undoneSymbol: "🌧 ", 7 | doneSymbol: "🌤 ", 8 | width: 10, 9 | formatString: "##bright##green#spinner##default #percent #bar #message", 10 | total: TOTAL, 11 | enableSpinner: true, 12 | lastUpdateForTiming: false 13 | }); 14 | 15 | const messages = [ 16 | "Starting weather machine", 17 | "Detecting bad weather", 18 | "Compressing raindrops", 19 | "Prepare some fresh sunbeams", 20 | "Enjoy the sunshine", 21 | "Done" 22 | ] 23 | 24 | 25 | function simulateProgress(current) { 26 | if (current <= TOTAL) { 27 | bar.update(current, messages[current/2]); 28 | setTimeout(() => simulateProgress(current + 1), 1000); 29 | } 30 | } 31 | 32 | simulateProgress(0); -------------------------------------------------------------------------------- /example/colors.js: -------------------------------------------------------------------------------- 1 | const AsciiBar = require('ascii-bar').default; 2 | 3 | const TOTAL = 40; 4 | 5 | const bar = new AsciiBar({ 6 | formatString: "#spinner ##red #count #percent ##default#bar |##bright##blue Time to finish: #ttf", 7 | total: TOTAL, 8 | enableSpinner: true 9 | }); 10 | 11 | 12 | function simulateProgress(current) { 13 | if(current > 12){ 14 | bar.formatString = "#spinner ##yellow #count #percent ##default#bar |##bright##blue Time to finish: #ttf"; 15 | } 16 | if(current > 27){ 17 | bar.formatString = "#spinner ##green #count #percent ##default#bar |##bright##blue Time to finish: #ttf"; 18 | } 19 | 20 | 21 | if (current <= TOTAL) { 22 | bar.update(current, "Currently at " + current); 23 | setTimeout(() => simulateProgress(current + 1), 200); 24 | } 25 | } 26 | 27 | simulateProgress(1); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ascii-bar", 3 | "version": "1.0.3", 4 | "description": "A zero dependency ascii progress bar with spinner, colors and typescript support", 5 | "main": "dist/AsciiBar.js", 6 | "types": "dist/AsciiBar.d.ts", 7 | "scripts": {}, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/superjojo140/ascii-bar.git" 11 | }, 12 | "keywords": [ 13 | "ascii", 14 | "progress", 15 | "bar", 16 | "console", 17 | "node", 18 | "stdout", 19 | "progressbar", 20 | "cli" 21 | ], 22 | "author": "superjojo140", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/superjojo140/ascii-bar/issues" 26 | }, 27 | "homepage": "https://github.com/superjojo140/ascii-bar#readme", 28 | "devDependencies": { 29 | "@types/node": "^14.11.10", 30 | "typescript": "^4.0.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/example.ts: -------------------------------------------------------------------------------- 1 | import AsciiBar from 'ascii-bar' 2 | import { simpleSpinner } from "ascii-bar" 3 | 4 | const TOTAL = 50; 5 | 6 | const bar = new AsciiBar({ 7 | undoneSymbol: "⋅", 8 | doneSymbol: ">", 9 | width: 20, 10 | formatString: " #spinner I'm a dotty spinner", 11 | total: TOTAL, 12 | enableSpinner: true, 13 | lastUpdateForTiming: false, 14 | autoStop: true, 15 | print: true, 16 | start: 0, 17 | startDate: new Date().getTime(), 18 | stream: process.stdout, 19 | hideCursor:true, 20 | }); 21 | 22 | 23 | 24 | 25 | function simulateProgress(current) { 26 | if (current > 24) { 27 | bar.spinner = simpleSpinner; 28 | bar.formatString = " #spinner I'm a simple spinner" 29 | } 30 | 31 | if (current >= TOTAL) { 32 | bar.stop(false); 33 | return; 34 | } 35 | 36 | if (current <= TOTAL) { 37 | bar.update(current, "Currently at " + current); 38 | setTimeout(() => simulateProgress(current + 1), 200); 39 | } 40 | } 41 | 42 | simulateProgress(1); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 superjojo140 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | //Generate screenshot.svg with: termtosvg -c "node example/example.js" -g "80x1" -t "window_frame" 2 | 3 | const AsciiBar = require('ascii-bar').default; 4 | const simpleSpinner = require('ascii-bar').simpleSpinner; 5 | 6 | const TOTAL1 = 30; 7 | const TOTAL2 = 40; 8 | const TOTAL3 = 10; 9 | 10 | let bar1, bar2, bar3; 11 | 12 | bar1 = new AsciiBar({ 13 | width: 30, 14 | formatString: "#spinner #percent #bar #message", 15 | total: TOTAL1, 16 | enableSpinner: true, 17 | lastUpdateForTiming: false, 18 | hideCursor: true, 19 | doneSymbol: "+", 20 | undoneSymbol: "-", 21 | }); 22 | bar1.spinner = simpleSpinner; 23 | 24 | 25 | function simulateProgress1(current) { 26 | if (current <= TOTAL1) { 27 | bar1.update(current, "Support for ascii only shells"); 28 | setTimeout(() => simulateProgress1(current + 1), 200); 29 | } 30 | else { 31 | setTimeout(() => { 32 | bar2 = new AsciiBar({ 33 | formatString: "#spinner ##red #count #percent ##default#bar |##blue Time to finish: #ttf", 34 | total: TOTAL2, 35 | enableSpinner: true, 36 | hideCursor: true, 37 | }); 38 | simulateProgress2(1); 39 | }, 1300); 40 | } 41 | } 42 | 43 | function simulateProgress2(current) { 44 | if (current > 12) { 45 | bar2.formatString = "#spinner ##yellow #count #percent ##default#bar |##blue Time to finish: #ttf"; 46 | } 47 | if (current > 27) { 48 | bar2.formatString = "#spinner ##green #count #percent ##default#bar |##blue Time to finish: #ttf"; 49 | } 50 | 51 | 52 | if (current <= TOTAL2) { 53 | bar2.update(current, "Currently at " + current); 54 | setTimeout(() => simulateProgress2(current + 1), 200); 55 | } 56 | else { 57 | setTimeout(() => { 58 | bar3 = new AsciiBar({ 59 | undoneSymbol: "🌧 ", 60 | doneSymbol: "🌤 ", 61 | width: 10, 62 | formatString: "#spinner##default #percent #bar #message", 63 | total: TOTAL3, 64 | enableSpinner: true, 65 | lastUpdateForTiming: false, 66 | hideCursor: true, 67 | }); 68 | simulateProgress3(1); 69 | }, 1300); 70 | } 71 | } 72 | 73 | const messages = [ 74 | "Starting weather machine", 75 | "Detecting bad weather", 76 | "Compressing raindrops", 77 | "Prepare some fresh sunbeams", 78 | "Enjoy the sunshine", 79 | "Done" 80 | ] 81 | 82 | 83 | function simulateProgress3(current) { 84 | if (current <= TOTAL3) { 85 | bar3.update(current, messages[current / 2]); 86 | setTimeout(() => simulateProgress3(current + 1), 1000); 87 | } 88 | } 89 | 90 | simulateProgress1(1); -------------------------------------------------------------------------------- /dist/AsciiBar.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default class AsciiBar { 3 | /** 4 | * Format of the displayed progressbar 5 | */ 6 | formatString: string; 7 | /** 8 | * Number of steps to finish progress 9 | */ 10 | total: number; 11 | /** 12 | * Startdate to calculate elapsed time (in milliseconds) 13 | */ 14 | startDate: number; 15 | /** 16 | * Which timespan to use for timing calculation - If you are unsure allways use false here! 17 | */ 18 | lastUpdateForTiming: boolean; 19 | /** 20 | * Width of the progress bar (only the #bar part) 21 | */ 22 | width: number; 23 | /** 24 | * Symbol for the done progress in the #bar part 25 | */ 26 | doneSymbol: string; 27 | /** 28 | * Symbol for the undone progress in the #bar part 29 | */ 30 | undoneSymbol: string; 31 | /** 32 | * Wether to print to configured stream or not 33 | */ 34 | print: boolean; 35 | /** 36 | * A spinner object describing how the spinner looks like 37 | * Change this for another spinner 38 | */ 39 | spinner: Spinner; 40 | /** 41 | * The message displayed at the #message placeholder 42 | */ 43 | message: string; 44 | /** 45 | * wether to call progressbar's stop() function automatically if the progress reaches 100% 46 | */ 47 | autoStop: boolean; 48 | /** 49 | * wether to hide the terminal's cursor while displaying the progress bar 50 | */ 51 | hideCursor: boolean; 52 | private elapsed; 53 | private lastUpdate; 54 | private timeToFinish; 55 | private overallTime; 56 | private stream; 57 | private spinnerTimeout; 58 | private enableSpinner; 59 | private currentSpinnerSymbol; 60 | private current; 61 | constructor(options?: string | ProgressbarOptions); 62 | /** 63 | * Creates the progressbar string with all configured settings 64 | * @returns a string representating the progressbar 65 | */ 66 | renderLine(): string; 67 | /** 68 | * Render the progressbar and print it to output stream 69 | */ 70 | printLine(): void; 71 | /** 72 | * update the progress. This will trigger re-rendering the progressbar 73 | * @param current the new absolute progress value 74 | * @param message [optional] update the message displayed at the #message placeholder 75 | */ 76 | update(current: number, message?: string): this; 77 | /** 78 | * Updates the spinner if enabled 79 | */ 80 | private updateSpinner; 81 | /** 82 | * Formats a time span (given in milliseconds) to a easy human readable string 83 | * @param millis timespan in milliseconds 84 | */ 85 | private formatTime; 86 | /** 87 | * Stop the progressbar 88 | * This will stop the spinner and change it's symbol to a checkmark (if not disabled) 89 | * Message will be changed to a string describing the elapsed time (if not disabled) 90 | * This function will be triggered automatically if the progressbar reaches 100% (if not disabled) 91 | * @param withInfo wether to auto-update the progressbar's spinner and message after stopping 92 | */ 93 | stop(withInfo?: boolean): void; 94 | } 95 | interface ProgressbarOptions { 96 | /** 97 | * Format of the displayed progressbar 98 | * Use serveral of this placeholders: 99 | * #bar #count #percent #overall #elapsed #ttf #message #spinner 100 | * And combine with serveral of this formatters: 101 | * ##default ##green ##blue ##red ##yellow ##bright ##dim 102 | * @default '#percent #bar' 103 | * @example '##bright##blue#spinner##default #percent #bar Elapsed: #elapsed Time to finish: #ttf #message' 104 | */ 105 | formatString?: string; 106 | /** 107 | * Number of steps to finish progress 108 | * @default 100 109 | */ 110 | total?: number; 111 | /** 112 | * Startdate to calculate elapsed time (in milliseconds) 113 | * Use this if the progress started before initialising the progressbar 114 | * @default 'new Date().getTime()' 115 | */ 116 | startDate?: number; 117 | /** 118 | * Stream to print the progressbar 119 | * @default process.stdout 120 | */ 121 | stream?: NodeJS.ReadWriteStream; 122 | /** 123 | * Width of the progress bar (only the #bar part) 124 | * @default 20 125 | */ 126 | width?: number; 127 | /** 128 | * Symbol for the done progress in the #bar part 129 | * @default '>' 130 | */ 131 | doneSymbol?: string; 132 | /** 133 | * Symbol for the undone progress in the #bar part 134 | * @default '-' 135 | */ 136 | undoneSymbol?: string; 137 | /** 138 | * Wether to print to configured stream or not 139 | * If set to false get the currently rendered statusbar with bar.renderLine() 140 | * @default true 141 | */ 142 | print?: boolean; 143 | /** 144 | * Start value of progress 145 | * @default 0 146 | */ 147 | start?: number; 148 | /** 149 | * Wether to enable the spinner update function or not. 150 | * If enabled the statusbar will re-render automatically every few seconds to update the spinner symbol 151 | * Make sure to include #spinner in formatString to use spinner symbol 152 | * @default false 153 | */ 154 | enableSpinner?: boolean; 155 | /** 156 | * Which timespan to use for timing calculation - If you are unsure allways use false here! 157 | * set to FALSE: Assume that each of the remaining steps will take as long as THE AVERAGE OF ALL the previous steps 158 | * set to TRUE: Assume that eah of the remaining steps will take as long as THE LAST STEP took. WARNING: This implies, that every call of the ProgressBar.update() function increment the state with the same stepwidth. 159 | * @default false using overall elapsed time for timing calculation 160 | */ 161 | lastUpdateForTiming?: boolean; 162 | /** 163 | * wether to call progressbar's stop() function automatically if the progress reaches 100% 164 | * @default true 165 | */ 166 | autoStop?: boolean; 167 | /** 168 | * wether to hide the terminal's cursor while displaying the progress bar 169 | * cursor will be re-enabled by the bar.stop() function 170 | * @default false 171 | */ 172 | hideCursor?: boolean; 173 | } 174 | export declare let defaultSpinner: Spinner; 175 | export declare let simpleSpinner: Spinner; 176 | interface Spinner { 177 | /** 178 | * Number of milliseconds to update to the next spinner frame 179 | */ 180 | interval: number; 181 | /** 182 | * Array of the spinner "frames" 183 | * A frame means one char 184 | */ 185 | frames: string[]; 186 | /** 187 | * Used in runtime to store currently displayed frame 188 | * @default 0 189 | */ 190 | currentFrame?: number; 191 | } 192 | export {}; 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ascii-bar 2 | 3 | ![Screenshot - made with termToSvg](https://raw.githubusercontent.com/superjojo140/ascii-bar/main/example/screenshot.svg) 4 | 5 | - [How to use](#how-to-use) 6 | - [Configuration](#configuration) 7 | - [Template String](#template-string) 8 | - [Options](#options) 9 | - [Spinner](#spinner) 10 | - [API Methods and properties](#api-methods-and-properties) 11 | - [Methods](#methods) 12 | - [Properties](#properties) 13 | 14 | ## Why is it cool? 15 | 16 | - 🚀 Extreme **lightweight** (<50kB) and **zero dependencies** 17 | - ⭕ **Fancy Spinners** (automatic ascii fallback for windows) 18 | - 🎨 **Colors** and Emoji support (if your terminal can display this) 19 | - 🖋️ Intuitive styling via **templateString** 20 | - ⏰ Calculation and pretty printing of overall progress time and **time to finish** 21 | - 🔧 Extreme customizable (configure output stream, timing calculation, spinner behavior,...) 22 | - 📖 Typescript types and documentation 23 | 24 | ## How to use 25 | 26 | #### Installation 27 | 28 | ````shell 29 | npm install ascii-bar 30 | ```` 31 | 32 | #### Basic Usage 33 | 34 | ````javascript 35 | const AsciiBar = require('ascii-bar').default; 36 | 37 | const bar = new AsciiBar(); 38 | 39 | //in your long during task 40 | bar.update(numberOfDoneThings,someInfoAboutCurrentTask); 41 | ```` 42 | 43 | #### Using with `import` 44 | 45 | ````javascript 46 | import AsciiBar from 'ascii-bar' 47 | ```` 48 | 49 | For more examples see [examples folder](https://github.com/superjojo140/ascii-bar/tree/main/example). 50 | 51 | ## Configuration 52 | 53 | ### Template string 54 | 55 | The `templateString` has the greatest influence on the appearance. It allows you to define which elements your status bar contains and how they are arranged. 56 | To use a special `templateString` use it as a parameter in the constructor: 57 | 58 | ````javascript 59 | const bar = new AsciiBar('#spinner #percent #bar Overall time: #overall ##blue #message'); 60 | ```` 61 | You can use and mix the following placeholders and modificators: 62 | 63 | | Placeholder | Description | Example | 64 | |-------------|-------------------------------------|---------------------| 65 | | #bar | The visualized progress bar | [>>>>>>>>------] | 66 | | #count | Count of done tasks and total tasks | [12/42] | 67 | | #percent | Percentage of done tasks | 30% | 68 | | #overall | Estimated overall time | 1h 12m | 69 | | #elapsed | Elapsed time | 1d 2h 34m | 70 | | #ttf | Estimated time to finish | 34m 13s | 71 | | #message | Information about the current task | Uploading dummy.txt | 72 | | #spinner | A spinner | ⠼ | 73 | | ##default | Reset text formatting | default text | 74 | | ##green | green text | green text | 75 | | ##blue | blue text | blue text | 76 | | ##red | red text | red text | 77 | | ##yellow | yellow text | yellow text | 78 | | ##bright | bright text | bright blue text | 79 | | ##dim | dimmed text | dimmed green text | 80 | 81 | 82 | ### Options 83 | 84 | You can also use a configuration object in the constructor: 85 | 86 | ````javascript 87 | const bar = new AsciiBar({ 88 | undoneSymbol: "-", 89 | doneSymbol: ">", 90 | width: 20, 91 | formatString: '#percent #bar', 92 | total: 100, 93 | enableSpinner: false, 94 | lastUpdateForTiming: false, 95 | autoStop : true, 96 | print: true, 97 | start: 0, 98 | startDate: new Date().getTime(), 99 | stream: process.stdout, 100 | hideCursor: false, 101 | }); 102 | ```` 103 | 104 | For more detailed explanation off all these options have a look at the [AsciiBar.d.ts](dist/AsciiBar.d.ts#L91) 105 | 106 | ### Spinner 107 | 108 | ![Screenshot - made with termToSvg](https://raw.githubusercontent.com/superjojo140/ascii-bar/main/example/spinner.svg) 109 | 110 | #### Use a spinner 111 | 112 | To use a spinner simply set the `enableSpinner` option to `true`. 113 | Also use the `#spinner` placeholder in your template string. 114 | 115 | Minimal example: 116 | 117 | ````javascript 118 | const bar = new AsciiBar({ 119 | formatString: '#spinner #percent #bar', 120 | enableSpinner: true 121 | }); 122 | ```` 123 | 124 | #### Modify spinner 125 | 126 | You can also set a [custom spinner](dist/AsciiBar.d.ts#L164) 127 | For more spinner inspiration see [cli-spinners](https://www.npmjs.com/package/cli-spinners) 128 | 129 | ````javascript 130 | bar.spinner = { 131 | interval: 100, 132 | frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] 133 | } 134 | ```` 135 | 136 | ## API Methods and properties 137 | 138 | ### Methods 139 | 140 | ````javascript 141 | /** 142 | * update the progress. This will trigger re-rendering the progressbar 143 | * @param current - the new absolute progress value 144 | * @param message - [optional] update the message displayed at the #message placeholder 145 | */ 146 | bar.update(current: number, message?: string) 147 | 148 | /** 149 | * Creates the progressbar string with all configured settings 150 | * @returns a string representating the progressbar 151 | */ 152 | bar.renderLine(): string 153 | 154 | /** 155 | * Stop the progressbar 156 | * This will stop the spinner and change it's symbol to a checkmark (if not disabled) 157 | * Message will be changed to a string describing the elapsed time (if not disabled) 158 | * This function will be triggered automatically if the progressbar reaches 100% (if not disabled) 159 | * @param withInfo - wether to auto-update the progressbar's spinner and message after stopping 160 | */ 161 | bar.stop(withInfo = true) 162 | ```` 163 | 164 | 165 | ### Properties 166 | 167 | All of this properties can be changed (even while the progressbar is running). 168 | 169 | E.g. to set a new message text do: 170 | 171 | ````javascript 172 | bar.message = "SomeNewText"; 173 | ```` 174 | 175 | ````javascript 176 | /** 177 | * Format of the displayed progressbar 178 | */ 179 | public formatString = '#percent #bar'; 180 | 181 | /** 182 | * Number of steps to finish progress 183 | */ 184 | public total = 100; 185 | 186 | /** 187 | * Startdate to calculate elapsed time (in milliseconds) 188 | */ 189 | public startDate = new Date().getTime(); 190 | 191 | /** 192 | * Which time span to use for timing calculation - If you are unsure always use false here! 193 | */ 194 | public lastUpdateForTiming = false; 195 | 196 | /** 197 | * Width of the progress bar (only the #bar part) 198 | */ 199 | public width = 20; 200 | 201 | /** 202 | * Symbol for the done progress in the #bar part 203 | */ 204 | public doneSymbol = ">"; 205 | 206 | /** 207 | * Symbol for the undone progress in the #bar part 208 | */ 209 | public undoneSymbol = "-"; 210 | 211 | /** 212 | * Wether to print to configured stream or not 213 | */ 214 | public print = true; 215 | 216 | /** 217 | * A spinner object describing how the spinner looks like 218 | * Change this for another spinner 219 | */ 220 | public spinner = defaultSpinner; 221 | 222 | /** 223 | * The message displayed at the #message placeholder 224 | */ 225 | public message = ""; 226 | 227 | /** 228 | * wether to call progressbar's stop() function automatically if the progress reaches 100% 229 | */ 230 | public autoStop = true; 231 | 232 | /** 233 | * wether to hide the terminal's cursor while displaying the progress bar 234 | * cursor will be re-enabled by the bar.stop() function 235 | * @default false 236 | */ 237 | hideCursor?: boolean; 238 | ```` 239 | -------------------------------------------------------------------------------- /dist/AsciiBar.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var AsciiBar = /** @class */ (function () { 4 | function AsciiBar(options) { 5 | var _this = this; 6 | /** 7 | * Format of the displayed progressbar 8 | */ 9 | this.formatString = '#percent #bar'; 10 | /** 11 | * Number of steps to finish progress 12 | */ 13 | this.total = 100; 14 | /** 15 | * Startdate to calculate elapsed time (in milliseconds) 16 | */ 17 | this.startDate = new Date().getTime(); 18 | /** 19 | * Which timespan to use for timing calculation - If you are unsure allways use false here! 20 | */ 21 | this.lastUpdateForTiming = false; 22 | /** 23 | * Width of the progress bar (only the #bar part) 24 | */ 25 | this.width = 20; 26 | /** 27 | * Symbol for the done progress in the #bar part 28 | */ 29 | this.doneSymbol = ">"; 30 | /** 31 | * Symbol for the undone progress in the #bar part 32 | */ 33 | this.undoneSymbol = "-"; 34 | /** 35 | * Wether to print to configured stream or not 36 | */ 37 | this.print = true; 38 | /** 39 | * A spinner object describing how the spinner looks like 40 | * Change this for another spinner 41 | */ 42 | this.spinner = exports.defaultSpinner; 43 | /** 44 | * The message displayed at the #message placeholder 45 | */ 46 | this.message = ""; 47 | /** 48 | * wether to call progressbar's stop() function automatically if the progress reaches 100% 49 | */ 50 | this.autoStop = true; 51 | /** 52 | * wether to hide the terminal's cursor while displaying the progress bar 53 | */ 54 | this.hideCursor = false; 55 | this.elapsed = 0; 56 | this.lastUpdate = new Date().getTime(); 57 | this.timeToFinish = 0; 58 | this.overallTime = 0; 59 | this.stream = process.stdout; 60 | this.enableSpinner = false; 61 | this.currentSpinnerSymbol = ""; 62 | this.current = 0; 63 | /** 64 | * Updates the spinner if enabled 65 | */ 66 | this.updateSpinner = function () { 67 | if (_this.spinner.currentFrame === undefined) { 68 | _this.spinner.currentFrame = 0; 69 | } 70 | _this.spinner.currentFrame = (_this.spinner.currentFrame + 1) % _this.spinner.frames.length; 71 | _this.currentSpinnerSymbol = _this.spinner.frames[_this.spinner.currentFrame]; 72 | _this.printLine(); 73 | _this.spinnerTimeout = setTimeout(_this.updateSpinner, _this.spinner.interval); 74 | }; 75 | if (options) { 76 | //if only a string was provided, use this as formatString 77 | if (typeof options == "string") { 78 | options = { formatString: options }; 79 | } 80 | //set other options 81 | for (var opt in options) { 82 | if (this[opt] !== undefined) { 83 | this[opt] = options[opt]; 84 | } 85 | } 86 | //set start value 87 | if (options.start) { 88 | this.current = options.start; 89 | } 90 | //use simple spinner on windows 91 | if (process.platform === 'win32') { 92 | this.spinner = exports.simpleSpinner; 93 | } 94 | //enable spinner 95 | if (this.enableSpinner) { 96 | this.spinnerTimeout = setTimeout(this.updateSpinner, this.spinner.interval); 97 | } 98 | } 99 | } 100 | /** 101 | * Creates the progressbar string with all configured settings 102 | * @returns a string representating the progressbar 103 | */ 104 | AsciiBar.prototype.renderLine = function () { 105 | var plusCount = Math.round(this.current / this.total * this.width); 106 | var minusCount = this.width - plusCount; 107 | var plusString = ""; 108 | var minusString = ""; 109 | for (var i = 0; i < plusCount; i++) { 110 | plusString += this.doneSymbol; 111 | } 112 | for (var i = 0; i < minusCount; i++) { 113 | minusString += this.undoneSymbol; 114 | } 115 | var barString = "[" + plusString + minusString + "]"; 116 | var currentString = String(this.current); 117 | while (currentString.length < String(this.total).length) { 118 | currentString = "0" + currentString; 119 | } 120 | var countString = "[" + currentString + "/" + this.total + "]"; 121 | var percentString = String(Math.round((this.current / this.total) * 100)); 122 | while (percentString.length < 3) { 123 | percentString = " " + percentString; 124 | } 125 | percentString += "%"; 126 | var overAllString = this.formatTime(this.overallTime); 127 | var elapsedString = this.formatTime(this.elapsed); 128 | var ttfString = this.formatTime(this.timeToFinish); 129 | //Replace macros 130 | var line = this.formatString.replace(/#bar/g, barString).replace(/#count/g, countString).replace(/#percent/g, percentString).replace(/#overall/g, overAllString).replace(/#elapsed/g, elapsedString).replace(/#ttf/g, ttfString).replace(/#message/g, this.message).replace(/#spinner/g, this.currentSpinnerSymbol); 131 | //Colors :-) 132 | line = line.replace(/##default/g, colorCodes.Reset).replace(/##green/g, colorCodes.Green).replace(/##blue/g, colorCodes.Blue).replace(/##red/g, colorCodes.Red).replace(/##yellow/g, colorCodes.Yellow).replace(/##bright/g, colorCodes.Bright).replace(/##dim/g, colorCodes.Dim); 133 | line += colorCodes.Reset; 134 | //Hide cursor 135 | if (this.hideCursor) { 136 | line = colorCodes.HideCursor + line; 137 | } 138 | return line; 139 | }; 140 | /** 141 | * Render the progressbar and print it to output stream 142 | */ 143 | AsciiBar.prototype.printLine = function () { 144 | if (!this.print) { 145 | return; 146 | } 147 | this.stream.cursorTo(0); 148 | this.stream.write(this.renderLine()); 149 | this.stream.clearLine(1); 150 | }; 151 | /** 152 | * update the progress. This will trigger re-rendering the progressbar 153 | * @param current the new absolute progress value 154 | * @param message [optional] update the message displayed at the #message placeholder 155 | */ 156 | AsciiBar.prototype.update = function (current, message) { 157 | this.current = current; 158 | if (message) { 159 | this.message = message; 160 | } 161 | //timePerTick * max = overallTime 162 | //timePerTick = elapsed / current 163 | //overallTime = (elapsed / current) * max 164 | //timeToFinish = overallTime - elapsed 165 | var now = new Date().getTime(); 166 | //how to calculate time per step 167 | var timePerStep = this.lastUpdateForTiming ? (now - this.lastUpdate) : (this.elapsed / this.current); 168 | this.elapsed = now - this.startDate; 169 | this.overallTime = (timePerStep * this.total); 170 | this.timeToFinish = (timePerStep * (this.total - this.current)); 171 | this.printLine(); 172 | //Stop if finished 173 | if (this.autoStop && (this.current / this.total >= 1)) { 174 | this.stop(); 175 | } 176 | this.lastUpdate = now; 177 | return this; 178 | }; 179 | /** 180 | * Formats a time span (given in milliseconds) to a easy human readable string 181 | * @param millis timespan in milliseconds 182 | */ 183 | AsciiBar.prototype.formatTime = function (millis) { 184 | //Milliseconds 185 | if (millis < 500) { 186 | return Math.round(millis) + "ms"; 187 | } 188 | //Seconds 189 | if (millis < 60 * 1000) { 190 | return Math.round(millis / 1000) + "s"; 191 | } 192 | //Minutes 193 | if (millis < 60 * 60 * 1000) { 194 | var minutes_1 = Math.round(millis / (1000 * 60)); 195 | var seconds = Math.round(millis % (1000 * 60) / 1000); 196 | return minutes_1 + "m " + seconds + "s"; 197 | } 198 | //Hours 199 | if (millis < 24 * 60 * 60 * 1000) { 200 | var hours_1 = Math.round(millis / (1000 * 60 * 60)); 201 | var minutes_2 = Math.round(millis % (1000 * 60 * 60) / (1000 * 60)); 202 | return hours_1 + "h " + minutes_2 + "m"; 203 | } 204 | //Days 205 | var days = Math.round(millis / (1000 * 60 * 60 * 24)); 206 | var hours = Math.round(millis % (1000 * 60 * 60 * 24) / (1000 * 60 * 60)); 207 | var minutes = Math.round(millis % (1000 * 60 * 60) / (1000 * 60)); 208 | return days + "d " + hours + "h " + minutes + "m"; 209 | }; 210 | /** 211 | * Stop the progressbar 212 | * This will stop the spinner and change it's symbol to a checkmark (if not disabled) 213 | * Message will be changed to a string describing the elapsed time (if not disabled) 214 | * This function will be triggered automatically if the progressbar reaches 100% (if not disabled) 215 | * @param withInfo wether to auto-update the progressbar's spinner and message after stopping 216 | */ 217 | AsciiBar.prototype.stop = function (withInfo) { 218 | if (withInfo === void 0) { withInfo = true; } 219 | //Stop the spinner 220 | if (this.spinnerTimeout) { 221 | clearTimeout(this.spinnerTimeout); 222 | } 223 | if (withInfo) { 224 | //change spinner to checkmark 225 | this.currentSpinnerSymbol = colorCodes.Green + colorCodes.Bright + "✓" + colorCodes.Reset; 226 | if (process.platform === 'win32') { 227 | this.currentSpinnerSymbol = "OK "; 228 | } 229 | ; 230 | //set overalltime to really elapsed time 231 | this.overallTime = this.elapsed; 232 | this.message = "Finished in " + this.formatTime(this.overallTime); 233 | this.printLine(); 234 | } 235 | //add newline and re-enable cursor 236 | console.log(this.hideCursor ? colorCodes.ShowCursor : ""); 237 | }; 238 | return AsciiBar; 239 | }()); 240 | exports.default = AsciiBar; 241 | //ColorCodes from https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color 242 | var colorCodes = { 243 | Reset: "\x1b[0m", 244 | Bright: "\x1b[1m", 245 | Dim: "\x1b[2m", 246 | Underscore: "\x1b[4m", 247 | Blink: "\x1b[5m", 248 | Reverse: "\x1b[7m", 249 | Hidden: "\x1b[8m", 250 | Black: "\x1b[30m", 251 | Red: "\x1b[31m", 252 | Green: "\x1b[32m", 253 | Yellow: "\x1b[33m", 254 | Blue: "\x1b[34m", 255 | Magenta: "\x1b[35m", 256 | Cyan: "\x1b[36m", 257 | White: "\x1b[37m", 258 | HideCursor: "\x1B[?25l", 259 | ShowCursor: "\x1B[?25h", 260 | }; 261 | exports.defaultSpinner = { 262 | interval: 120, 263 | frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] 264 | }; 265 | exports.simpleSpinner = { 266 | interval: 120, 267 | frames: ["-", "\\", "|", "/"] 268 | }; 269 | -------------------------------------------------------------------------------- /src/AsciiBar.ts: -------------------------------------------------------------------------------- 1 | export default class AsciiBar { 2 | /** 3 | * Format of the displayed progressbar 4 | */ 5 | public formatString = '#percent #bar'; 6 | 7 | /** 8 | * Number of steps to finish progress 9 | */ 10 | public total = 100; 11 | 12 | /** 13 | * Startdate to calculate elapsed time (in milliseconds) 14 | */ 15 | public startDate = new Date().getTime(); 16 | 17 | /** 18 | * Which timespan to use for timing calculation - If you are unsure allways use false here! 19 | */ 20 | public lastUpdateForTiming = false; 21 | 22 | /** 23 | * Width of the progress bar (only the #bar part) 24 | */ 25 | public width = 20; 26 | 27 | /** 28 | * Symbol for the done progress in the #bar part 29 | */ 30 | public doneSymbol = ">"; 31 | 32 | /** 33 | * Symbol for the undone progress in the #bar part 34 | */ 35 | public undoneSymbol = "-"; 36 | 37 | /** 38 | * Wether to print to configured stream or not 39 | */ 40 | public print = true; 41 | 42 | /** 43 | * A spinner object describing how the spinner looks like 44 | * Change this for another spinner 45 | */ 46 | public spinner = defaultSpinner; 47 | 48 | /** 49 | * The message displayed at the #message placeholder 50 | */ 51 | public message = ""; 52 | 53 | /** 54 | * wether to call progressbar's stop() function automatically if the progress reaches 100% 55 | */ 56 | public autoStop = true; 57 | 58 | /** 59 | * wether to hide the terminal's cursor while displaying the progress bar 60 | */ 61 | public hideCursor = false; 62 | 63 | private elapsed = 0; 64 | private lastUpdate = new Date().getTime(); 65 | private timeToFinish = 0; 66 | private overallTime = 0; 67 | private stream = process.stdout; 68 | private spinnerTimeout; 69 | private enableSpinner = false; 70 | private currentSpinnerSymbol = ""; 71 | private current = 0; 72 | 73 | constructor(options?: string | ProgressbarOptions) { 74 | if (options) { 75 | //if only a string was provided, use this as formatString 76 | if (typeof options == "string") { 77 | options = { formatString: options } 78 | } 79 | 80 | //set other options 81 | for (const opt in options) { 82 | if (this[opt] !== undefined) { 83 | this[opt] = options[opt]; 84 | } 85 | } 86 | 87 | //set start value 88 | if (options.start) { 89 | this.current = options.start; 90 | } 91 | 92 | //use simple spinner on windows 93 | if (process.platform === 'win32') { 94 | this.spinner = simpleSpinner; 95 | } 96 | 97 | //enable spinner 98 | if (this.enableSpinner) { 99 | this.spinnerTimeout = setTimeout(this.updateSpinner, this.spinner.interval); 100 | } 101 | } 102 | } 103 | 104 | 105 | /** 106 | * Creates the progressbar string with all configured settings 107 | * @returns a string representating the progressbar 108 | */ 109 | public renderLine(): string { 110 | let plusCount = Math.round(this.current / this.total * this.width); 111 | let minusCount = this.width - plusCount; 112 | let plusString = ""; 113 | let minusString = ""; 114 | for (let i = 0; i < plusCount; i++) { plusString += this.doneSymbol; } 115 | for (let i = 0; i < minusCount; i++) { minusString += this.undoneSymbol; } 116 | let barString = `[${plusString}${minusString}]`; 117 | 118 | let currentString = String(this.current); 119 | while (currentString.length < String(this.total).length) { currentString = "0" + currentString; } 120 | let countString = `[${currentString}/${this.total}]`; 121 | 122 | let percentString = String(Math.round((this.current / this.total) * 100)); 123 | while (percentString.length < 3) { percentString = " " + percentString; } 124 | percentString += "%"; 125 | 126 | let overAllString = this.formatTime(this.overallTime); 127 | let elapsedString = this.formatTime(this.elapsed); 128 | let ttfString = this.formatTime(this.timeToFinish); 129 | 130 | //Replace macros 131 | let line = this.formatString.replace(/#bar/g, barString).replace(/#count/g, countString).replace(/#percent/g, percentString).replace(/#overall/g, overAllString).replace(/#elapsed/g, elapsedString).replace(/#ttf/g, ttfString).replace(/#message/g, this.message).replace(/#spinner/g, this.currentSpinnerSymbol); 132 | 133 | //Colors :-) 134 | line = line.replace(/##default/g, colorCodes.Reset).replace(/##green/g, colorCodes.Green).replace(/##blue/g, colorCodes.Blue).replace(/##red/g, colorCodes.Red).replace(/##yellow/g, colorCodes.Yellow).replace(/##bright/g, colorCodes.Bright).replace(/##dim/g, colorCodes.Dim); 135 | line += colorCodes.Reset; 136 | 137 | //Hide cursor 138 | if (this.hideCursor) { 139 | line = colorCodes.HideCursor + line; 140 | } 141 | 142 | return line; 143 | } 144 | 145 | /** 146 | * Render the progressbar and print it to output stream 147 | */ 148 | public printLine(): void { 149 | if (!this.print) { return; } 150 | this.stream.cursorTo(0); 151 | this.stream.write(this.renderLine()); 152 | this.stream.clearLine(1); 153 | } 154 | 155 | /** 156 | * update the progress. This will trigger re-rendering the progressbar 157 | * @param current the new absolute progress value 158 | * @param message [optional] update the message displayed at the #message placeholder 159 | */ 160 | public update(current: number, message?: string) { 161 | this.current = current; 162 | 163 | if (message) { this.message = message; } 164 | 165 | //timePerTick * max = overallTime 166 | //timePerTick = elapsed / current 167 | //overallTime = (elapsed / current) * max 168 | //timeToFinish = overallTime - elapsed 169 | let now = new Date().getTime(); 170 | //how to calculate time per step 171 | let timePerStep = this.lastUpdateForTiming ? (now - this.lastUpdate) : (this.elapsed / this.current); 172 | this.elapsed = now - this.startDate; 173 | this.overallTime = (timePerStep * this.total); 174 | this.timeToFinish = (timePerStep * (this.total - this.current)); 175 | this.printLine() 176 | 177 | //Stop if finished 178 | if (this.autoStop && (this.current / this.total >= 1)) { this.stop() } 179 | 180 | this.lastUpdate = now; 181 | return this 182 | } 183 | 184 | /** 185 | * Updates the spinner if enabled 186 | */ 187 | private updateSpinner = () => { 188 | if (this.spinner.currentFrame === undefined) { this.spinner.currentFrame = 0 } 189 | this.spinner.currentFrame = (this.spinner.currentFrame + 1) % this.spinner.frames.length; 190 | this.currentSpinnerSymbol = this.spinner.frames[this.spinner.currentFrame]; 191 | this.printLine(); 192 | this.spinnerTimeout = setTimeout(this.updateSpinner, this.spinner.interval); 193 | } 194 | 195 | /** 196 | * Formats a time span (given in milliseconds) to a easy human readable string 197 | * @param millis timespan in milliseconds 198 | */ 199 | private formatTime(millis: number): string { 200 | //Milliseconds 201 | if (millis < 500) { 202 | return `${Math.round(millis)}ms`; 203 | } 204 | //Seconds 205 | if (millis < 60 * 1000) { 206 | return `${Math.round(millis / 1000)}s`; 207 | } 208 | //Minutes 209 | if (millis < 60 * 60 * 1000) { 210 | let minutes = Math.round(millis / (1000 * 60)); 211 | let seconds = Math.round(millis % (1000 * 60) / 1000); 212 | return `${minutes}m ${seconds}s`; 213 | } 214 | //Hours 215 | if (millis < 24 * 60 * 60 * 1000) { 216 | let hours = Math.round(millis / (1000 * 60 * 60)); 217 | let minutes = Math.round(millis % (1000 * 60 * 60) / (1000 * 60)); 218 | return `${hours}h ${minutes}m`; 219 | } 220 | //Days 221 | let days = Math.round(millis / (1000 * 60 * 60 * 24)); 222 | let hours = Math.round(millis % (1000 * 60 * 60 * 24) / (1000 * 60 * 60)); 223 | let minutes = Math.round(millis % (1000 * 60 * 60) / (1000 * 60)); 224 | return `${days}d ${hours}h ${minutes}m`; 225 | } 226 | 227 | /** 228 | * Stop the progressbar 229 | * This will stop the spinner and change it's symbol to a checkmark (if not disabled) 230 | * Message will be changed to a string describing the elapsed time (if not disabled) 231 | * This function will be triggered automatically if the progressbar reaches 100% (if not disabled) 232 | * @param withInfo wether to auto-update the progressbar's spinner and message after stopping 233 | */ 234 | public stop(withInfo = true) { 235 | //Stop the spinner 236 | if (this.spinnerTimeout) { 237 | clearTimeout(this.spinnerTimeout); 238 | } 239 | 240 | if (withInfo) { 241 | //change spinner to checkmark 242 | this.currentSpinnerSymbol = colorCodes.Green + colorCodes.Bright + "✓" + colorCodes.Reset; 243 | if (process.platform === 'win32') { this.currentSpinnerSymbol = "OK " }; 244 | //set overalltime to really elapsed time 245 | this.overallTime = this.elapsed; 246 | this.message = `Finished in ${this.formatTime(this.overallTime)}` 247 | this.printLine(); 248 | } 249 | 250 | //add newline and re-enable cursor 251 | console.log(this.hideCursor ? colorCodes.ShowCursor : ""); 252 | } 253 | } 254 | 255 | interface ProgressbarOptions { 256 | /** 257 | * Format of the displayed progressbar 258 | * Use serveral of this placeholders: 259 | * #bar #count #percent #overall #elapsed #ttf #message #spinner 260 | * And combine with serveral of this formatters: 261 | * ##default ##green ##blue ##red ##yellow ##bright ##dim 262 | * @default '#percent #bar' 263 | * @example '##bright##blue#spinner##default #percent #bar Elapsed: #elapsed Time to finish: #ttf #message' 264 | */ 265 | 266 | formatString?: string; 267 | 268 | /** 269 | * Number of steps to finish progress 270 | * @default 100 271 | */ 272 | total?: number; 273 | 274 | /** 275 | * Startdate to calculate elapsed time (in milliseconds) 276 | * Use this if the progress started before initialising the progressbar 277 | * @default 'new Date().getTime()' 278 | */ 279 | startDate?: number; 280 | 281 | /** 282 | * Stream to print the progressbar 283 | * @default process.stdout 284 | */ 285 | stream?: NodeJS.ReadWriteStream; 286 | 287 | /** 288 | * Width of the progress bar (only the #bar part) 289 | * @default 20 290 | */ 291 | width?: number; 292 | 293 | /** 294 | * Symbol for the done progress in the #bar part 295 | * @default '>' 296 | */ 297 | doneSymbol?: string; 298 | 299 | /** 300 | * Symbol for the undone progress in the #bar part 301 | * @default '-' 302 | */ 303 | undoneSymbol?: string; 304 | 305 | /** 306 | * Wether to print to configured stream or not 307 | * If set to false get the currently rendered statusbar with bar.renderLine() 308 | * @default true 309 | */ 310 | print?: boolean; 311 | 312 | /** 313 | * Start value of progress 314 | * @default 0 315 | */ 316 | start?: number; 317 | 318 | /** 319 | * Wether to enable the spinner update function or not. 320 | * If enabled the statusbar will re-render automatically every few seconds to update the spinner symbol 321 | * Make sure to include #spinner in formatString to use spinner symbol 322 | * @default false 323 | */ 324 | enableSpinner?: boolean; 325 | 326 | /** 327 | * Which timespan to use for timing calculation - If you are unsure allways use false here! 328 | * set to FALSE: Assume that each of the remaining steps will take as long as THE AVERAGE OF ALL the previous steps 329 | * set to TRUE: Assume that eah of the remaining steps will take as long as THE LAST STEP took. WARNING: This implies, that every call of the ProgressBar.update() function increment the state with the same stepwidth. 330 | * @default false using overall elapsed time for timing calculation 331 | */ 332 | lastUpdateForTiming?: boolean; 333 | 334 | /** 335 | * wether to call progressbar's stop() function automatically if the progress reaches 100% 336 | * @default true 337 | */ 338 | autoStop?: boolean; 339 | 340 | /** 341 | * wether to hide the terminal's cursor while displaying the progress bar 342 | * cursor will be re-enabled by the bar.stop() function 343 | * @default false 344 | */ 345 | hideCursor?: boolean; 346 | } 347 | 348 | //ColorCodes from https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color 349 | const colorCodes = { 350 | Reset: "\x1b[0m", 351 | Bright: "\x1b[1m", 352 | Dim: "\x1b[2m", 353 | Underscore: "\x1b[4m", 354 | Blink: "\x1b[5m", 355 | Reverse: "\x1b[7m", 356 | Hidden: "\x1b[8m", 357 | 358 | Black: "\x1b[30m", 359 | Red: "\x1b[31m", 360 | Green: "\x1b[32m", 361 | Yellow: "\x1b[33m", 362 | Blue: "\x1b[34m", 363 | Magenta: "\x1b[35m", 364 | Cyan: "\x1b[36m", 365 | White: "\x1b[37m", 366 | 367 | HideCursor: "\x1B[?25l", 368 | ShowCursor: "\x1B[?25h", 369 | } 370 | 371 | export let defaultSpinner: Spinner = { 372 | interval: 120, 373 | frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] 374 | } 375 | 376 | export let simpleSpinner: Spinner = { 377 | interval: 120, 378 | frames: ["-", "\\", "|", "/"] 379 | } 380 | 381 | interface Spinner { 382 | /** 383 | * Number of milliseconds to update to the next spinner frame 384 | */ 385 | interval: number; 386 | 387 | /** 388 | * Array of the spinner "frames" 389 | * A frame means one char 390 | */ 391 | frames: string[]; 392 | 393 | /** 394 | * Used in runtime to store currently displayed frame 395 | * @default 0 396 | */ 397 | currentFrame?: number; 398 | } 399 | -------------------------------------------------------------------------------- /example/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 276 | 299 | 300 | 301 | 302 | 303 | 304 | I'm a dotty spinner ⠙ I'm a dotty spinner ⠹ I'm a dotty spinner ⠸ I'm a dotty spinner ⠼ I'm a dotty spinner ⠴ I'm a dotty spinner ⠦ I'm a dotty spinner ⠧ I'm a dotty spinner ⠇ I'm a dotty spinner ⠏ I'm a dotty spinner ⠋ I'm a dotty spinner ⠏ I'm a simple spinner \ I'm a simple spinner | I'm a simple spinner / I'm a simple spinner - I'm a simple spinner 305 | -------------------------------------------------------------------------------- /example/screenshot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 555 | 578 | 579 | 580 | 581 | 582 | 583 | 3% [+-----------------------------] Support for ascii only shells \ 3% [+-----------------------------] Support for ascii only shells \ 7% [++----------------------------] Support for ascii only shells | 7% [++----------------------------] Support for ascii only shells / 7% [++----------------------------] Support for ascii only shells / 10% [+++---------------------------] Support for ascii only shells - 10% [+++---------------------------] Support for ascii only shells \ 13% [++++--------------------------] Support for ascii only shells | 13% [++++--------------------------] Support for ascii only shells | 17% [+++++-------------------------] Support for ascii only shells / 17% [+++++-------------------------] Support for ascii only shells - 17% [+++++-------------------------] Support for ascii only shells - 20% [++++++------------------------] Support for ascii only shells \ 20% [++++++------------------------] Support for ascii only shells \ 23% [+++++++-----------------------] Support for ascii only shells | 23% [+++++++-----------------------] Support for ascii only shells / 23% [+++++++-----------------------] Support for ascii only shells / 27% [++++++++----------------------] Support for ascii only shells - 27% [++++++++----------------------] Support for ascii only shells \ 27% [++++++++----------------------] Support for ascii only shells \ 30% [+++++++++---------------------] Support for ascii only shells | 30% [+++++++++---------------------] Support for ascii only shells | 33% [++++++++++--------------------] Support for ascii only shells / 33% [++++++++++--------------------] Support for ascii only shells - 33% [++++++++++--------------------] Support for ascii only shells - 37% [+++++++++++-------------------] Support for ascii only shells \ 37% [+++++++++++-------------------] Support for ascii only shells | 37% [+++++++++++-------------------] Support for ascii only shells | 40% [++++++++++++------------------] Support for ascii only shells / 40% [++++++++++++------------------] Support for ascii only shells / 43% [+++++++++++++-----------------] Support for ascii only shells - 43% [+++++++++++++-----------------] Support for ascii only shells \ 43% [+++++++++++++-----------------] Support for ascii only shells \ 47% [++++++++++++++----------------] Support for ascii only shells | 47% [++++++++++++++----------------] Support for ascii only shells / 47% [++++++++++++++----------------] Support for ascii only shells / 50% [+++++++++++++++---------------] Support for ascii only shells - 50% [+++++++++++++++---------------] Support for ascii only shells - 53% [++++++++++++++++--------------] Support for ascii only shells \ 53% [++++++++++++++++--------------] Support for ascii only shells | 53% [++++++++++++++++--------------] Support for ascii only shells | 57% [+++++++++++++++++-------------] Support for ascii only shells / 57% [+++++++++++++++++-------------] Support for ascii only shells - 57% [+++++++++++++++++-------------] Support for ascii only shells - 60% [++++++++++++++++++------------] Support for ascii only shells \ 60% [++++++++++++++++++------------] Support for ascii only shells \ 63% [+++++++++++++++++++-----------] Support for ascii only shells | 63% [+++++++++++++++++++-----------] Support for ascii only shells / 63% [+++++++++++++++++++-----------] Support for ascii only shells / 67% [++++++++++++++++++++----------] Support for ascii only shells - 67% [++++++++++++++++++++----------] Support for ascii only shells \ 67% [++++++++++++++++++++----------] Support for ascii only shells \ 70% [+++++++++++++++++++++---------] Support for ascii only shells | 70% [+++++++++++++++++++++---------] Support for ascii only shells | 73% [++++++++++++++++++++++--------] Support for ascii only shells / 73% [++++++++++++++++++++++--------] Support for ascii only shells - 73% [++++++++++++++++++++++--------] Support for ascii only shells - 77% [+++++++++++++++++++++++-------] Support for ascii only shells \ 77% [+++++++++++++++++++++++-------] Support for ascii only shells | 77% [+++++++++++++++++++++++-------] Support for ascii only shells | 80% [++++++++++++++++++++++++------] Support for ascii only shells / 80% [++++++++++++++++++++++++------] Support for ascii only shells / 83% [+++++++++++++++++++++++++-----] Support for ascii only shells - 83% [+++++++++++++++++++++++++-----] Support for ascii only shells \ 83% [+++++++++++++++++++++++++-----] Support for ascii only shells \ 87% [++++++++++++++++++++++++++----] Support for ascii only shells | 87% [++++++++++++++++++++++++++----] Support for ascii only shells / 87% [++++++++++++++++++++++++++----] Support for ascii only shells / 90% [+++++++++++++++++++++++++++---] Support for ascii only shells - 90% [+++++++++++++++++++++++++++---] Support for ascii only shells - 93% [++++++++++++++++++++++++++++--] Support for ascii only shells \ 93% [++++++++++++++++++++++++++++--] Support for ascii only shells | 93% [++++++++++++++++++++++++++++--] Support for ascii only shells | 97% [+++++++++++++++++++++++++++++-] Support for ascii only shells / 97% [+++++++++++++++++++++++++++++-] Support for ascii only shells - 97% [+++++++++++++++++++++++++++++-] Support for ascii only shells OK 100% [++++++++++++++++++++++++++++++] Finished in 6s [01/40] 3% [>-------------------] | Time to finish: 0ms [01/40] 3% [>-------------------] | Time to finish: 0ms [02/40] 5% [>-------------------] | Time to finish: 0ms [02/40] 5% [>-------------------] | Time to finish: 0ms [02/40] 5% [>-------------------] | Time to finish: 0ms [03/40] 8% [>>------------------] | Time to finish: 2s [03/40] 8% [>>------------------] | Time to finish: 2s [04/40] 10% [>>------------------] | Time to finish: 4s [04/40] 10% [>>------------------] | Time to finish: 4s [05/40] 13% [>>>-----------------] | Time to finish: 4s [05/40] 13% [>>>-----------------] | Time to finish: 4s [05/40] 13% [>>>-----------------] | Time to finish: 4s [06/40] 15% [>>>-----------------] | Time to finish: 5s [06/40] 15% [>>>-----------------] | Time to finish: 5s [07/40] 18% [>>>>----------------] | Time to finish: 5s [07/40] 18% [>>>>----------------] | Time to finish: 5s [08/40] 20% [>>>>----------------] | Time to finish: 5s [08/40] 20% [>>>>----------------] | Time to finish: 5s [08/40] 20% [>>>>----------------] | Time to finish: 5s [09/40] 23% [>>>>>---------------] | Time to finish: 5s [09/40] 23% [>>>>>---------------] | Time to finish: 5s [10/40] 25% [>>>>>---------------] | Time to finish: 5s [10/40] 25% [>>>>>---------------] | Time to finish: 5s [11/40] 28% [>>>>>>--------------] | Time to finish: 5s [11/40] 28% [>>>>>>--------------] | Time to finish: 5s [11/40] 28% [>>>>>>--------------] | Time to finish: 5s [12/40] 30% [>>>>>>--------------] | Time to finish: 5s [12/40] 30% [>>>>>>--------------] | Time to finish: 5s [13/40] 33% [>>>>>>>-------------] | Time to finish: 5s [13/40] 33% [>>>>>>>-------------] | Time to finish: 5s [13/40] 33% [>>>>>>>-------------] | Time to finish: 5s [14/40] 35% [>>>>>>>-------------] | Time to finish: 4s [14/40] 35% [>>>>>>>-------------] | Time to finish: 4s [14/40] 35% [>>>>>>>-------------] | Time to finish: 4s [15/40] 38% [>>>>>>>>------------] | Time to finish: 4s [15/40] 38% [>>>>>>>>------------] | Time to finish: 4s [16/40] 40% [>>>>>>>>------------] | Time to finish: 4s [16/40] 40% [>>>>>>>>------------] | Time to finish: 4s [16/40] 40% [>>>>>>>>------------] | Time to finish: 4s [17/40] 43% [>>>>>>>>>-----------] | Time to finish: 4s [17/40] 43% [>>>>>>>>>-----------] | Time to finish: 4s [17/40] 43% [>>>>>>>>>-----------] | Time to finish: 4s [18/40] 45% [>>>>>>>>>-----------] | Time to finish: 4s [18/40] 45% [>>>>>>>>>-----------] | Time to finish: 4s [19/40] 48% [>>>>>>>>>>----------] | Time to finish: 4s [19/40] 48% [>>>>>>>>>>----------] | Time to finish: 4s [19/40] 48% [>>>>>>>>>>----------] | Time to finish: 4s [20/40] 50% [>>>>>>>>>>----------] | Time to finish: 4s [20/40] 50% [>>>>>>>>>>----------] | Time to finish: 4s [20/40] 50% [>>>>>>>>>>----------] | Time to finish: 4s [21/40] 53% [>>>>>>>>>>>---------] | Time to finish: 3s [21/40] 53% [>>>>>>>>>>>---------] | Time to finish: 3s [22/40] 55% [>>>>>>>>>>>---------] | Time to finish: 3s [22/40] 55% [>>>>>>>>>>>---------] | Time to finish: 3s [22/40] 55% [>>>>>>>>>>>---------] | Time to finish: 3s [23/40] 57% [>>>>>>>>>>>>--------] | Time to finish: 3s [23/40] 57% [>>>>>>>>>>>>--------] | Time to finish: 3s [23/40] 57% [>>>>>>>>>>>>--------] | Time to finish: 3s [24/40] 60% [>>>>>>>>>>>>--------] | Time to finish: 3s [24/40] 60% [>>>>>>>>>>>>--------] | Time to finish: 3s [25/40] 63% [>>>>>>>>>>>>>-------] | Time to finish: 3s [25/40] 63% [>>>>>>>>>>>>>-------] | Time to finish: 3s [25/40] 63% [>>>>>>>>>>>>>-------] | Time to finish: 3s [26/40] 65% [>>>>>>>>>>>>>-------] | Time to finish: 3s [26/40] 65% [>>>>>>>>>>>>>-------] | Time to finish: 3s [26/40] 65% [>>>>>>>>>>>>>-------] | Time to finish: 3s [27/40] 68% [>>>>>>>>>>>>>>------] | Time to finish: 2s [27/40] 68% [>>>>>>>>>>>>>>------] | Time to finish: 2s [28/40] 70% [>>>>>>>>>>>>>>------] | Time to finish: 2s [28/40] 70% [>>>>>>>>>>>>>>------] | Time to finish: 2s [28/40] 70% [>>>>>>>>>>>>>>------] | Time to finish: 2s [29/40] 73% [>>>>>>>>>>>>>>>-----] | Time to finish: 2s [29/40] 73% [>>>>>>>>>>>>>>>-----] | Time to finish: 2s [29/40] 73% [>>>>>>>>>>>>>>>-----] | Time to finish: 2s [30/40] 75% [>>>>>>>>>>>>>>>-----] | Time to finish: 2s [30/40] 75% [>>>>>>>>>>>>>>>-----] | Time to finish: 2s [31/40] 78% [>>>>>>>>>>>>>>>>----] | Time to finish: 2s [31/40] 78% [>>>>>>>>>>>>>>>>----] | Time to finish: 2s [31/40] 78% [>>>>>>>>>>>>>>>>----] | Time to finish: 2s [32/40] 80% [>>>>>>>>>>>>>>>>----] | Time to finish: 2s [32/40] 80% [>>>>>>>>>>>>>>>>----] | Time to finish: 2s [32/40] 80% [>>>>>>>>>>>>>>>>----] | Time to finish: 2s [33/40] 83% [>>>>>>>>>>>>>>>>>---] | Time to finish: 1s [33/40] 83% [>>>>>>>>>>>>>>>>>---] | Time to finish: 1s [34/40] 85% [>>>>>>>>>>>>>>>>>---] | Time to finish: 1s [34/40] 85% [>>>>>>>>>>>>>>>>>---] | Time to finish: 1s [34/40] 85% [>>>>>>>>>>>>>>>>>---] | Time to finish: 1s [35/40] 88% [>>>>>>>>>>>>>>>>>>--] | Time to finish: 1s [35/40] 88% [>>>>>>>>>>>>>>>>>>--] | Time to finish: 1s [35/40] 88% [>>>>>>>>>>>>>>>>>>--] | Time to finish: 1s [36/40] 90% [>>>>>>>>>>>>>>>>>>--] | Time to finish: 1s [36/40] 90% [>>>>>>>>>>>>>>>>>>--] | Time to finish: 1s [37/40] 93% [>>>>>>>>>>>>>>>>>>>-] | Time to finish: 1s [37/40] 93% [>>>>>>>>>>>>>>>>>>>-] | Time to finish: 1s [37/40] 93% [>>>>>>>>>>>>>>>>>>>-] | Time to finish: 1s [38/40] 95% [>>>>>>>>>>>>>>>>>>>-] | Time to finish: 381ms [38/40] 95% [>>>>>>>>>>>>>>>>>>>-] | Time to finish: 381ms [38/40] 95% [>>>>>>>>>>>>>>>>>>>-] | Time to finish: 381ms [39/40] 98% [>>>>>>>>>>>>>>>>>>>>] | Time to finish: 191ms [39/40] 98% [>>>>>>>>>>>>>>>>>>>>] | Time to finish: 191ms [40/40] 100% [>>>>>>>>>>>>>>>>>>>>] | Time to finish: 0ms 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠴ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠦ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠧ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠇ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠏ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠋ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠙ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠹ 10% [🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] ⠹ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠸ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠼ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠴ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠦ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠧ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠇ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠏ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠋ 20% [🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠋ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠙ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠹ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠸ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠼ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠴ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠦ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠧ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠇ 30% [🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 🌧 ] Detecting bad weather ⠇ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠏ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠋ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠙ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠹ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠸ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠼ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠴ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠦ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠧ 40% [🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠧ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠇ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠏ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠋ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠙ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠹ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠸ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠼ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠴ 50% [🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 🌧 ] Compressing raindrops ⠴ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠦ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠧ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠇ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠏ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠋ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠙ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠹ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠸ 60% [🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠸ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠼ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠴ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠦ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠧ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠇ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠏ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠋ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠙ 70% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 🌧 ] Prepare some fresh sunbeams ⠙ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠹ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠸ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠼ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠴ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠦ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠧ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠇ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠏ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠋ 80% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 🌧 ] Enjoy the sunshine ⠋ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠙ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠹ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠸ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠼ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠴ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠦ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠧ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine ⠇ 90% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌧 ] Enjoy the sunshine 100% [🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 🌤 ] Finished in 9s 584 | --------------------------------------------------------------------------------