1 | 'use strict'
|
2 |
|
3 |
|
4 |
|
5 | const stripAnsi = require('strip-ansi')
|
6 | let console = require('./console')
|
7 | let color = require('@heroku-cli/color').default
|
8 | let errors = require('./errors')
|
9 |
|
10 | class 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 |
|
145 | Spinner.prompts = []
|
146 |
|
147 | module.exports = Spinner
|