index.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. 'use strict';
  2. const chalk = require('chalk');
  3. const cliCursor = require('cli-cursor');
  4. const cliSpinners = require('cli-spinners');
  5. const logSymbols = require('log-symbols');
  6. const stripAnsi = require('strip-ansi');
  7. const wcwidth = require('wcwidth');
  8. const TEXT = Symbol('text');
  9. const PREFIX_TEXT = Symbol('prefixText');
  10. class Ora {
  11. constructor(options) {
  12. if (typeof options === 'string') {
  13. options = {
  14. text: options
  15. };
  16. }
  17. this.options = Object.assign({
  18. text: '',
  19. color: 'cyan',
  20. stream: process.stderr
  21. }, options);
  22. this.spinner = this.options.spinner;
  23. this.color = this.options.color;
  24. this.hideCursor = this.options.hideCursor !== false;
  25. this.interval = this.options.interval || this.spinner.interval || 100;
  26. this.stream = this.options.stream;
  27. this.id = null;
  28. this.isEnabled = typeof this.options.isEnabled === 'boolean' ? this.options.isEnabled : ((this.stream && this.stream.isTTY) && !process.env.CI);
  29. // Set *after* `this.stream`
  30. this.text = this.options.text;
  31. this.prefixText = this.options.prefixText;
  32. this.linesToClear = 0;
  33. this.indent = this.options.indent;
  34. }
  35. get indent() {
  36. return this._indent;
  37. }
  38. set indent(indent = 0) {
  39. if (!(indent >= 0 && Number.isInteger(indent))) {
  40. throw new Error('The `indent` option must be an integer from 0 and up');
  41. }
  42. this._indent = indent;
  43. }
  44. get spinner() {
  45. return this._spinner;
  46. }
  47. set spinner(spinner) {
  48. this.frameIndex = 0;
  49. if (typeof spinner === 'object') {
  50. if (spinner.frames === undefined) {
  51. throw new Error('The given spinner must have a `frames` property');
  52. }
  53. this._spinner = spinner;
  54. } else if (process.platform === 'win32') {
  55. this._spinner = cliSpinners.line;
  56. } else if (spinner === undefined) {
  57. // Set default spinner
  58. this._spinner = cliSpinners.dots;
  59. } else if (cliSpinners[spinner]) {
  60. this._spinner = cliSpinners[spinner];
  61. } else {
  62. throw new Error(`There is no built-in spinner named '${spinner}'. See https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json for a full list.`);
  63. }
  64. }
  65. get text() {
  66. return this[TEXT];
  67. }
  68. get prefixText() {
  69. return this[PREFIX_TEXT];
  70. }
  71. get isSpinning() {
  72. return this.id !== null;
  73. }
  74. updateLineCount() {
  75. const columns = this.stream.columns || 80;
  76. const fullPrefixText = (typeof this[PREFIX_TEXT] === 'string') ? this[PREFIX_TEXT] + '-' : '';
  77. this.lineCount = stripAnsi(fullPrefixText + '--' + this[TEXT]).split('\n').reduce((count, line) => {
  78. return count + Math.max(1, Math.ceil(wcwidth(line) / columns));
  79. }, 0);
  80. }
  81. set text(value) {
  82. this[TEXT] = value;
  83. this.updateLineCount();
  84. }
  85. set prefixText(value) {
  86. this[PREFIX_TEXT] = value;
  87. this.updateLineCount();
  88. }
  89. frame() {
  90. const {frames} = this.spinner;
  91. let frame = frames[this.frameIndex];
  92. if (this.color) {
  93. frame = chalk[this.color](frame);
  94. }
  95. this.frameIndex = ++this.frameIndex % frames.length;
  96. const fullPrefixText = typeof this.prefixText === 'string' ? this.prefixText + ' ' : '';
  97. const fullText = typeof this.text === 'string' ? ' ' + this.text : '';
  98. return fullPrefixText + frame + fullText;
  99. }
  100. clear() {
  101. if (!this.isEnabled || !this.stream.isTTY) {
  102. return this;
  103. }
  104. for (let i = 0; i < this.linesToClear; i++) {
  105. if (i > 0) {
  106. this.stream.moveCursor(0, -1);
  107. }
  108. this.stream.clearLine();
  109. this.stream.cursorTo(this.indent);
  110. }
  111. this.linesToClear = 0;
  112. return this;
  113. }
  114. render() {
  115. this.clear();
  116. this.stream.write(this.frame());
  117. this.linesToClear = this.lineCount;
  118. return this;
  119. }
  120. start(text) {
  121. if (text) {
  122. this.text = text;
  123. }
  124. if (!this.isEnabled) {
  125. this.stream.write(`- ${this.text}\n`);
  126. return this;
  127. }
  128. if (this.isSpinning) {
  129. return this;
  130. }
  131. if (this.hideCursor) {
  132. cliCursor.hide(this.stream);
  133. }
  134. this.render();
  135. this.id = setInterval(this.render.bind(this), this.interval);
  136. return this;
  137. }
  138. stop() {
  139. if (!this.isEnabled) {
  140. return this;
  141. }
  142. clearInterval(this.id);
  143. this.id = null;
  144. this.frameIndex = 0;
  145. this.clear();
  146. if (this.hideCursor) {
  147. cliCursor.show(this.stream);
  148. }
  149. return this;
  150. }
  151. succeed(text) {
  152. return this.stopAndPersist({symbol: logSymbols.success, text});
  153. }
  154. fail(text) {
  155. return this.stopAndPersist({symbol: logSymbols.error, text});
  156. }
  157. warn(text) {
  158. return this.stopAndPersist({symbol: logSymbols.warning, text});
  159. }
  160. info(text) {
  161. return this.stopAndPersist({symbol: logSymbols.info, text});
  162. }
  163. stopAndPersist(options = {}) {
  164. const prefixText = options.prefixText || this.prefixText;
  165. const fullPrefixText = (typeof prefixText === 'string') ? prefixText + ' ' : '';
  166. const text = options.text || this.text;
  167. const fullText = (typeof text === 'string') ? ' ' + text : '';
  168. this.stop();
  169. this.stream.write(`${fullPrefixText}${options.symbol || ' '}${fullText}\n`);
  170. return this;
  171. }
  172. }
  173. const oraFactory = function (opts) {
  174. return new Ora(opts);
  175. };
  176. module.exports = oraFactory;
  177. // TODO: Remove this for the next major release
  178. module.exports.default = oraFactory;
  179. module.exports.promise = (action, options) => {
  180. if (typeof action.then !== 'function') {
  181. throw new TypeError('Parameter `action` must be a Promise');
  182. }
  183. const spinner = new Ora(options);
  184. spinner.start();
  185. action.then(
  186. () => {
  187. spinner.succeed();
  188. },
  189. () => {
  190. spinner.fail();
  191. }
  192. );
  193. return spinner;
  194. };