UNPKG

3.96 kBJavaScriptView Raw
1'use strict'
2
3// code mostly from https://github.com/sindresorhus/ora
4
5const stripAnsi = require('strip-ansi')
6let console = require('./console')
7let color = require('@heroku-cli/color').default
8let errors = require('./errors')
9
10class Spinner {
11 constructor (options) {
12 this.options = Object.assign({
13 text: ''
14 }, options)
15
16 this.ansi = require('ansi-escapes')
17 let spinners = require('./spinners.json')
18
19 this.color = this.options.color || 'heroku'
20 this.spinner = process.platform === 'win32' ? spinners.line : (this.options.spinner ? spinners[this.options.spinner] : spinners.dots2)
21 this.text = this.options.text
22 this.interval = this.options.interval || this.spinner.interval || 100
23 this.id = null
24 this.frameIndex = 0
25 this.stream = this.options.stream || process.stderr
26 this.enabled = !console.mocking() && (this.stream && this.stream.isTTY) && !process.env.CI && process.env.TERM !== 'dumb'
27 this.warnings = []
28 }
29
30 start () {
31 if (this.id) return
32 if (!this.enabled) {
33 console.writeError(this.text)
34 return
35 }
36
37 this.stream.write(this.ansi.cursorLeft)
38 this.stream.write(this.ansi.eraseLine)
39 this.stream.write(this.ansi.cursorHide)
40 this._render()
41 this.id = setInterval(this._spin.bind(this), this.interval)
42 process.on('SIGWINCH', this._sigwinch = this._render.bind(this))
43 }
44
45 stop (status) {
46 if (status && !this.enabled) console.error(` ${status}`)
47 if (!this.enabled) return
48 if (status) this._status = status
49
50 process.removeListener('SIGWINCH', this._sigwinch)
51 clearInterval(this.id)
52 this.id = null
53 this.enabled = false
54 this.frameIndex = 0
55 this._render()
56 this.stream.write(this.ansi.cursorShow)
57 }
58
59 warn (msg) {
60 if (!this.enabled) {
61 console.writeError(color.yellow(' !') + '\n' + errors.renderWarning(msg) + '\n' + this.text)
62 } else {
63 this.warnings.push(msg)
64 this._render()
65 }
66 }
67
68 get status () {
69 return this._status
70 }
71
72 set status (status) {
73 this._status = status
74 if (this.enabled) this._render()
75 else console.writeError(` ${this.status}\n${this.text}`)
76 }
77
78 clear () {
79 if (!this._output) return
80 this.stream.write(this.ansi.cursorUp(this._lines(this._output)))
81 this.stream.write(this.ansi.eraseDown)
82 }
83
84 _render () {
85 if (this._output) this.clear()
86 this._output = `${this.text}${this.enabled ? ' ' + this._frame() : ''} ${this.status ? this.status : ''}\n` +
87 this.warnings.map(w => errors.renderWarning(w) + '\n').join('')
88 this.stream.write(this._output)
89 }
90
91 _lines (s) {
92 return stripAnsi(s)
93 .split('\n')
94 .map(l => Math.ceil(l.length / this._width))
95 .reduce((c, i) => c + i, 0)
96 }
97
98 get _width () {
99 return errors.errtermwidth()
100 }
101
102 _spin () {
103 if (Spinner.prompts.length > 0) {
104 return
105 }
106
107 this.stream.write(this.ansi.cursorUp(this._lines(this._output)))
108 let y = this._lines(this.text) - 1
109 let lastline = stripAnsi(this.text).split('\n').pop()
110 let x = 1 + lastline.length - (this._lines(lastline) - 1) * this._width
111 this.stream.write(this.ansi.cursorMove(x, y))
112 this.stream.write(this._frame())
113 this.stream.write(this.ansi.cursorDown(this._lines(this._output) - y))
114 this.stream.write(this.ansi.cursorLeft)
115 this.stream.write(this.ansi.eraseLine)
116 }
117
118 _frame () {
119 var frames = this.spinner.frames
120 var frame = frames[this.frameIndex]
121 if (this.color) frame = color[this.color](frame)
122 this.frameIndex = ++this.frameIndex % frames.length
123 return frame
124 }
125
126 static prompt (promptFn) {
127 let removeFn = function () {
128 Spinner.prompts = Spinner.prompts.filter(p => p !== promptFn)
129 }
130
131 Spinner.prompts.push(promptFn)
132
133 return promptFn()
134 .then(data => {
135 removeFn()
136 return data
137 })
138 .catch(err => {
139 removeFn()
140 throw err
141 })
142 }
143}
144
145Spinner.prompts = []
146
147module.exports = Spinner