├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── example.js ├── index.js ├── media ├── loader.gif └── preview.gif └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | node_js: 5 | - 8 6 | - 6 7 | - 4 8 | 9 | install: 10 | - npm install 11 | 12 | script: 13 | - npm test 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | 9 | ## [1.1.0] - 2018-01-11 10 | ### Changed 11 | - [asciify-image](https://www.npmjs.com/package/asciify-image) is now used as a fallback instead of [progress-bar](https://www.npmjs.com/package/progress-bar). 12 | - [lodash.isequal](https://www.npmjs.com/package/lodash.isequal) is now used for checking if objects are equal rather than loading the entire [lodash](https://www.npmjs.com/package/lodash) library. 13 | 14 | ### Removed 15 | - `fallback` parameter for all 3 functions. 16 | 17 | 18 | ## [1.0.2] - 2017-09-29 19 | ### Added 20 | - Support for node versions 4.0 through 5.12. 21 | 22 | 23 | ## [1.0.1] - 2017-09-18 24 | ### Added 25 | - [CHANGELOG.md](CHANGELOG.md). 26 | - Note about iTerm version requirement to [README.md](README.md). 27 | 28 | ### Changed 29 | - Fixed path to [loader.gif](loader.gif). 30 | - Switched to absolute preview image URL in [README.md](README.md) so that it will show up on NPM. 31 | 32 | 33 | ## 1.0.0 - 2017-09-18 34 | ### Added 35 | - Initial release. 36 | 37 | [Unreleased]: https://github.com/kodie/progress-img/compare/v1.0.0...HEAD 38 | [1.1.0]: https://github.com/kodie/progress-img/compare/v1.0.2...v1.1.0 39 | [1.0.2]: https://github.com/kodie/progress-img/compare/v1.0.1...v1.0.2 40 | [1.0.1]: https://github.com/kodie/progress-img/compare/v1.0.0...v1.0.1 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kodie Grantham 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 | # progress-img 2 | [![npm package version](https://img.shields.io/npm/v/progress-img.svg?style=flat-square)](https://www.npmjs.com/package/progress-img) 3 | [![Travis build status](https://img.shields.io/travis/kodie/progress-img.svg?style=flat-square)](https://travis-ci.org/kodie/progress-img) 4 | [![npm package downloads](https://img.shields.io/npm/dt/progress-img.svg?style=flat-square)](https://www.npmjs.com/package/progress-img) 5 | [![index.js file size](https://img.shields.io/github/size/kodie/progress-img/index.js.svg?style=flat-square)](index.js) 6 | [![code style](https://img.shields.io/badge/code_style-standard-yellow.svg?style=flat-square)](https://github.com/standard/standard) 7 | [![license](https://img.shields.io/github/license/kodie/progress-img.svg?style=flat-square)](LICENSE.md) 8 | 9 | ![](https://raw.githubusercontent.com/kodie/progress-img/master/media/preview.gif?raw=true) 10 | 11 | Use images as progress bars in the terminal! 12 | 13 | ## Requirements 14 | * [Node.js](https://nodejs.org) v4.0 or higher 15 | 16 | *[iTerm2](https://iterm2.com) v2.9 or higher is required to display actual images. Otherwise an [ASCII](https://en.wikipedia.org/wiki/ASCII_art) version of the images will be displayed.* 17 | 18 | ## Installation 19 | ```shell 20 | npm install --save progress-img 21 | ``` 22 | 23 | ## Usage 24 | ```javascript 25 | const ProgressImg = require('progress-img') 26 | 27 | var progress = new ProgressImg('awesome.gif', { 28 | frameThrottle: '500ms', 29 | textTop: 'Downloading file...', 30 | width: '100%' 31 | }) 32 | 33 | // Set progress to a specific frame 34 | progress.set(3) 35 | 36 | // Set progress to a percentage 37 | progress.set('26%') 38 | 39 | // Display some text below the image 40 | progress.set('52%', { textBottom: 'Please wait...' }) 41 | 42 | // Clear the progress 43 | progress.clear() 44 | 45 | // Finish 46 | progress.done() 47 | ``` 48 | 49 | *Note: The `clear()` function is optional, however the `done()` function should always be ran when you are finished with the progress image.* 50 | 51 | ## Images 52 | The first parameter of the initial `ProgressImg` setup function is where you set the image(s) that you would like to use. If one is not supplied, a default one will be used. 53 | 54 | This parameter accepts a string or an array filled with strings that contain either a file path, URL, or image buffer. 55 | 56 | ## Options 57 | These options can be passed as the second parameter to either the initial `ProgressImg` setup, or to the `progress.set` function. 58 | 59 | ```javascript 60 | var options = { 61 | image: 'awesome.gif', 62 | 63 | width: 'auto', 64 | 65 | height: 'auto', 66 | 67 | preserveAspectRatio: true, 68 | 69 | expandGifs: false, 70 | 71 | useFallback: true, 72 | 73 | textTop: 'Downloading file...', 74 | 75 | textBottom: 'Please wait...', 76 | 77 | saveOptions: true, 78 | 79 | set: '20%', 80 | 81 | frameThrottle: '500ms', 82 | 83 | output: process.stdout 84 | } 85 | ``` 86 | 87 | ### image 88 | The image(s) to use. Can be used instead of the `image` parameter. (Defaults to `./loader.gif`) 89 | 90 | *Note: This option can only be used in the initial `ProgressImg` setup.* 91 | 92 | ### width 93 | ### height 94 | The width and height of the image. Can be one of the following: (Defaults to `auto`) 95 | 96 | * `N`: N character cells. 97 | * `Npx`: N pixels. 98 | * `N%`: N percent of the session's width or height. 99 | * `auto`: The image's inherent size will be used to determine an appropriate dimension. 100 | 101 | *Note: This option can only be used in the initial `ProgressImg` setup when using the ASCII fallback.* 102 | 103 | ### preserveAspectRatio 104 | Whether to preserve the aspect ratio of the image or not. (Defaults to `true`) 105 | 106 | ### expandGifs 107 | Whether or not to expand GIFs into separate frames. (Defaults to `true`) 108 | 109 | *Note: This option can only be used in the initial `ProgressImg` setup.* 110 | 111 | ### useFallback 112 | Whether to use the fallback progress bar regardless if the user is using a supported terminal or not. Great for testing how the fallback progress bar looks or bypassing the terminal check. (Defaults to `null`) 113 | 114 | *Note: This option can only be used in the initial `ProgressImg` setup.* 115 | 116 | ### textTop 117 | ### textBottom 118 | Text to display above or below the image. (Defaults to `null`) 119 | 120 | ### saveOptions 121 | Whether to overwrite the options set in the initial `ProgressImg` setup. (Defaults to `false`) 122 | 123 | *Note: This option can only be used in the `progress.set` function.* 124 | 125 | ### set 126 | The frame number or percentage to set the progress to. Can be used instead of `set` parameter. Can also be used in the initial `ProgressImg` setup. (Defaults to `null`) 127 | 128 | ### frameThrottle 129 | Throttle the frame changes by: (Defaults to `0`) 130 | 131 | * `N`: Actual frame count. 132 | * `N%`: Frame count percentage. 133 | * `Nms`: Milliseconds since the last frame change. 134 | 135 | *Note: `500ms` is recommended for a smoother animation.* 136 | 137 | ### output 138 | The stream to output to. (Defaults to `process.stdout`) 139 | 140 | ## License 141 | MIT. See the [License file](LICENSE.md) for more info. 142 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const http = require('http') 4 | const ProgressImg = require('.') 5 | 6 | var progress = new ProgressImg({ 7 | textTop: 'Downloading file...', 8 | frameThrottle: '500ms' 9 | }) 10 | 11 | var req = http.request({ 12 | host: 'ipv4.download.thinkbroadband.com', 13 | path: '/20MB.zip', // Available options: 1GB, 512MB, 200MB, 100MB, 50MB, 20MB, 10MB, 5MB 14 | port: 80 15 | }) 16 | 17 | req.on('response', res => { 18 | var total = parseInt(res.headers['content-length'], 10) 19 | var loaded = 0 20 | 21 | res.on('data', chunk => { 22 | loaded += chunk.length 23 | var percent = Math.round((loaded / total) * 100) + '%' 24 | 25 | progress.set(percent, { textBottom: percent + ' downloaded so far' }) 26 | }) 27 | 28 | res.on('end', () => { 29 | progress.clear().done() 30 | console.log('Download complete :)') 31 | }) 32 | }) 33 | 34 | req.end() 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ansiEscapes = require('ansi-escapes') 4 | const asciify = require('asciify-image') 5 | const Buffer = require('safe-buffer').Buffer 6 | const deasync = require('deasync') 7 | const fs = require('fs') 8 | const gifFrames = require('gif-frames') 9 | const imageType = require('image-type') 10 | const isEqual = require('lodash.isequal') 11 | const iterm2Version = require('iterm2-version') 12 | const logUpdate = require('log-update') 13 | const parseDataUri = require('parse-data-uri') 14 | const path = require('path') 15 | const toArray = require('stream-to-array') 16 | const request = require('request') 17 | 18 | const asciifySync = deasync(asciify) 19 | const gifFramesSync = deasync(gifFrames) 20 | const toArraySync = deasync(toArray) 21 | const requestSync = deasync(request) 22 | 23 | const defaults = { 24 | image: path.join(__dirname, 'media', 'loader.gif'), 25 | width: 'auto', 26 | height: 'auto', 27 | preserveAspectRatio: true, 28 | expandGifs: true, 29 | useFallback: null, 30 | textTop: null, 31 | textBottom: null, 32 | saveOptions: false, 33 | set: null, 34 | frameThrottle: 0, 35 | output: process.stdout 36 | } 37 | 38 | var ProgressImg = function (img, opts) { 39 | if (!opts && typeof img === 'object' && !Array.isArray(img)) { 40 | opts = img 41 | } 42 | 43 | this.cleared = false 44 | this.current = null 45 | this.fallback = false 46 | this.frames = [] 47 | this.fallbackFrames = [] 48 | this.lastSet = null 49 | this.log = null 50 | this.options = Object.assign({}, defaults, opts) 51 | this.prevOptions = {} 52 | 53 | if (!img) { 54 | img = this.options.image 55 | } 56 | 57 | if (!(img && img.length > 0)) { 58 | img = defaults.image 59 | } 60 | 61 | if (!Array.isArray(img)) { 62 | img = [img] 63 | } 64 | 65 | if (this.options.useFallback !== false) { 66 | if (this.options.useFallback || process.env.TERM_PROGRAM !== 'iTerm.app') { 67 | this.fallback = true 68 | } else { 69 | const iTermVersion = iterm2Version() 70 | 71 | if (Number(iTermVersion[0]) < 3) { 72 | this.fallback = true 73 | } 74 | } 75 | } 76 | 77 | if (this.fallback) { 78 | var fallbackFit = this.options.preserveAspectRatio ? 'box' : 'original' 79 | 80 | if (this.options.height === 'auto' && this.options.width === 'auto') { 81 | fallbackFit = 'box' 82 | } else if (this.options.height === 'auto') { 83 | fallbackFit = 'width' 84 | } else if (this.options.width === 'auto') { 85 | fallbackFit = 'height' 86 | } 87 | 88 | var fallbackOptions = { 89 | fit: fallbackFit, 90 | height: this.options.height === 'auto' ? '100%' : this.options.height.replace('px', ''), 91 | width: this.options.width === 'auto' ? '100%' : this.options.width.replace('px', '') 92 | } 93 | } 94 | 95 | this.log = logUpdate.create(this.options.output) 96 | 97 | img.forEach(i => { 98 | var buffer 99 | 100 | if (Buffer.isBuffer(i)) { 101 | buffer = i 102 | } else if (i.indexOf('data:') === 0) { 103 | var parsed = parseDataUri(i) 104 | 105 | if (parsed) { 106 | buffer = parsed.data 107 | } 108 | } else if (i.indexOf('http://') === 0 || i.indexOf('https://') === 0) { 109 | var response = requestSync({ url: i, encoding: null }) 110 | 111 | if (response) { 112 | buffer = response.body 113 | } 114 | } else { 115 | var contents = fs.readFileSync(i) 116 | 117 | if (contents) { 118 | buffer = Buffer.from(contents) 119 | } 120 | } 121 | 122 | var type = imageType(buffer) 123 | 124 | if (!type || !type.ext) { 125 | return 126 | } 127 | 128 | if (type.ext === 'gif' && this.options.extractGifs !== false) { 129 | try { 130 | var frames = gifFramesSync({ url: buffer, frames: 'all', outputType: 'png' }) 131 | 132 | frames.forEach(frame => { 133 | var parts = toArraySync(frame.getImage()) 134 | var frameBuffers = parts.map(part => Buffer.from(part)) 135 | var frameBuffer = Buffer.concat(frameBuffers) 136 | 137 | this.frames.push(frameBuffer) 138 | 139 | if (this.fallback) { 140 | this.fallbackFrames.push(asciifySync(frameBuffer, fallbackOptions)) 141 | } 142 | }) 143 | } catch (err) { 144 | throw err 145 | } 146 | } else { 147 | this.frames.push(buffer) 148 | 149 | if (this.fallback) { 150 | this.fallbackFrames.push(asciifySync(buffer, fallbackOptions)) 151 | } 152 | } 153 | }) 154 | 155 | if (!this.frames.length) { 156 | delete this.options.image 157 | return new ProgressImg(null, opts) 158 | } 159 | 160 | if (this.options.set) { 161 | this.set(this.options.set) 162 | } 163 | 164 | return this 165 | } 166 | 167 | ProgressImg.prototype.set = function (set, opts) { 168 | if (!opts && typeof set === 'object') { 169 | opts = set 170 | set = null 171 | } 172 | 173 | if (!set && !(opts && opts.set)) { 174 | set = 0 175 | } else if (!set) { 176 | set = opts.set 177 | } 178 | 179 | opts = Object.assign({}, this.options, opts) 180 | 181 | var frameNumber = set 182 | var frameLimit = ((this.fallback ? this.fallbackFrames.length : this.frames.length) - 1) 183 | 184 | if (opts.saveOptions) { 185 | opts.saveOptions = false 186 | this.options = opts 187 | } 188 | 189 | if (String(set).slice(-1) === '%') { 190 | frameNumber = Math.round((set.slice(0, -1) / 100) * frameLimit) 191 | } 192 | 193 | if (frameNumber > frameLimit) { 194 | frameNumber = frameLimit 195 | } 196 | 197 | if (frameNumber < 0) { 198 | frameNumber = 0 199 | } 200 | 201 | if (!this.cleared) { 202 | if (frameNumber === this.current && isEqual(this.prevOptions, opts)) { 203 | return this 204 | } 205 | 206 | if (opts.frameThrottle && set !== 0) { 207 | var frameThrottle = opts.frameThrottle 208 | 209 | if (String(frameThrottle).slice(-2) === 'ms') { 210 | frameThrottle = frameThrottle.slice(0, -2) 211 | 212 | if (!((Date.now() - this.lastSet) > frameThrottle)) { 213 | return this 214 | } 215 | } else { 216 | if (String(frameThrottle).slice(-1) === '%') { 217 | frameThrottle = Math.round((frameThrottle.slice(0, -1) / 100) * frameLimit) 218 | } 219 | 220 | if (!((frameNumber - this.current) > frameThrottle)) { 221 | return this 222 | } 223 | } 224 | } 225 | } 226 | 227 | this.cleared = false 228 | this.current = frameNumber 229 | this.lastSet = Date.now() 230 | this.prevOptions = opts 231 | 232 | var content 233 | 234 | if (this.fallback) { 235 | content = this.fallbackFrames[frameNumber] 236 | } else { 237 | content = ansiEscapes.image(this.frames[frameNumber], opts) 238 | } 239 | 240 | if (opts.textTop) { 241 | content = opts.textTop + '\n' + content 242 | } 243 | 244 | if (opts.textBottom) { 245 | content += '\n' + opts.textBottom 246 | } 247 | 248 | this.log(content) 249 | 250 | return this 251 | } 252 | 253 | ProgressImg.prototype.clear = function () { 254 | this.log.clear() 255 | this.cleared = true 256 | return this 257 | } 258 | 259 | ProgressImg.prototype.done = function () { 260 | this.log.done() 261 | return this 262 | } 263 | 264 | module.exports = ProgressImg 265 | -------------------------------------------------------------------------------- /media/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/progress-img/c092bfac84856b1eacdbb61c8063aa5c4af9a3e5/media/loader.gif -------------------------------------------------------------------------------- /media/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kodie/progress-img/c092bfac84856b1eacdbb61c8063aa5c4af9a3e5/media/preview.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "progress-img", 3 | "version": "1.1.0", 4 | "description": "Use images as progress bars in the terminal!", 5 | "keywords": [ 6 | "ascii", 7 | "art", 8 | "gif", 9 | "png", 10 | "jpg", 11 | "jpeg", 12 | "bmp", 13 | "tiff", 14 | "progress", 15 | "bar", 16 | "img", 17 | "image", 18 | "pic", 19 | "picture", 20 | "load", 21 | "loading", 22 | "loader", 23 | "stream", 24 | "animate", 25 | "animated", 26 | "animation", 27 | "buffer", 28 | "buffering" 29 | ], 30 | "author": { 31 | "name": "Kodie Grantham", 32 | "email": "kodie.grantham@gmail.com", 33 | "url": "http://kodieg.com" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/kodie/progress-img.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/kodie/progress-img/issues" 41 | }, 42 | "homepage": "https://github.com/kodie/progress-img", 43 | "license": "MIT", 44 | "main": "index.js", 45 | "scripts": { 46 | "example": "node example.js", 47 | "test": "standard" 48 | }, 49 | "engines": { 50 | "node": ">=4.0.0" 51 | }, 52 | "dependencies": { 53 | "ansi-escapes": "^3.0.0", 54 | "asciify-image": "^0.1.0", 55 | "deasync": "^0.1.10", 56 | "gif-frames": "^0.3.0", 57 | "image-type": "^3.0.0", 58 | "iterm2-version": "^2.3.0", 59 | "lodash.isequal": "^4.5.0", 60 | "log-update": "^2.1.0", 61 | "parse-data-uri": "^0.2.0", 62 | "request": "^2.81.0", 63 | "safe-buffer": "^5.1.1", 64 | "stream-to-array": "^2.3.0" 65 | }, 66 | "devDependencies": { 67 | "standard": "^10.0.3" 68 | } 69 | } 70 | --------------------------------------------------------------------------------