UNPKG

4.67 kBJavaScriptView Raw
1'use strict';
2const _ = {
3 last: require('lodash/last'),
4 flatten: require('lodash/flatten'),
5};
6const util = require('./readline');
7const cliWidth = require('cli-width');
8const stripAnsi = require('strip-ansi');
9const stringWidth = require('string-width');
10const ora = require('ora');
11
12function height(content) {
13 return content.split('\n').length;
14}
15
16function lastLine(content) {
17 return _.last(content.split('\n'));
18}
19
20class ScreenManager {
21 constructor(rl) {
22 // These variables are keeping information to allow correct prompt re-rendering
23 this.height = 0;
24 this.extraLinesUnderPrompt = 0;
25
26 this.rl = rl;
27 }
28
29 renderWithSpinner(content, bottomContent) {
30 if (this.spinnerId) {
31 clearInterval(this.spinnerId);
32 }
33
34 let spinner;
35 let contentFunc;
36 let bottomContentFunc;
37
38 if (bottomContent) {
39 spinner = ora(bottomContent);
40 contentFunc = () => content;
41 bottomContentFunc = () => spinner.frame();
42 } else {
43 spinner = ora(content);
44 contentFunc = () => spinner.frame();
45 bottomContentFunc = () => '';
46 }
47
48 this.spinnerId = setInterval(
49 () => this.render(contentFunc(), bottomContentFunc(), true),
50 spinner.interval
51 );
52 }
53
54 render(content, bottomContent, spinning = false) {
55 if (this.spinnerId && !spinning) {
56 clearInterval(this.spinnerId);
57 }
58
59 this.rl.output.unmute();
60 this.clean(this.extraLinesUnderPrompt);
61
62 /**
63 * Write message to screen and setPrompt to control backspace
64 */
65
66 const promptLine = lastLine(content);
67 const rawPromptLine = stripAnsi(promptLine);
68
69 // Remove the rl.line from our prompt. We can't rely on the content of
70 // rl.line (mainly because of the password prompt), so just rely on it's
71 // length.
72 let prompt = rawPromptLine;
73 if (this.rl.line.length) {
74 prompt = prompt.slice(0, -this.rl.line.length);
75 }
76
77 this.rl.setPrompt(prompt);
78
79 // SetPrompt will change cursor position, now we can get correct value
80 const cursorPos = this.rl._getCursorPos();
81 const width = this.normalizedCliWidth();
82
83 content = this.forceLineReturn(content, width);
84 if (bottomContent) {
85 bottomContent = this.forceLineReturn(bottomContent, width);
86 }
87
88 // Manually insert an extra line if we're at the end of the line.
89 // This prevent the cursor from appearing at the beginning of the
90 // current line.
91 if (rawPromptLine.length % width === 0) {
92 content += '\n';
93 }
94
95 const fullContent = content + (bottomContent ? '\n' + bottomContent : '');
96 this.rl.output.write(fullContent);
97
98 /**
99 * Re-adjust the cursor at the correct position.
100 */
101
102 // We need to consider parts of the prompt under the cursor as part of the bottom
103 // content in order to correctly cleanup and re-render.
104 const promptLineUpDiff = Math.floor(rawPromptLine.length / width) - cursorPos.rows;
105 const bottomContentHeight =
106 promptLineUpDiff + (bottomContent ? height(bottomContent) : 0);
107 if (bottomContentHeight > 0) {
108 util.up(this.rl, bottomContentHeight);
109 }
110
111 // Reset cursor at the beginning of the line
112 util.left(this.rl, stringWidth(lastLine(fullContent)));
113
114 // Adjust cursor on the right
115 if (cursorPos.cols > 0) {
116 util.right(this.rl, cursorPos.cols);
117 }
118
119 /**
120 * Set up state for next re-rendering
121 */
122 this.extraLinesUnderPrompt = bottomContentHeight;
123 this.height = height(fullContent);
124
125 this.rl.output.mute();
126 }
127
128 clean(extraLines) {
129 if (extraLines > 0) {
130 util.down(this.rl, extraLines);
131 }
132
133 util.clearLine(this.rl, this.height);
134 }
135
136 done() {
137 this.rl.setPrompt('');
138 this.rl.output.unmute();
139 this.rl.output.write('\n');
140 }
141
142 releaseCursor() {
143 if (this.extraLinesUnderPrompt > 0) {
144 util.down(this.rl, this.extraLinesUnderPrompt);
145 }
146 }
147
148 normalizedCliWidth() {
149 const width = cliWidth({
150 defaultWidth: 80,
151 output: this.rl.output,
152 });
153 return width;
154 }
155
156 breakLines(lines, width) {
157 // Break lines who're longer than the cli width so we can normalize the natural line
158 // returns behavior across terminals.
159 width = width || this.normalizedCliWidth();
160 const regex = new RegExp('(?:(?:\\033[[0-9;]*m)*.?){1,' + width + '}', 'g');
161 return lines.map((line) => {
162 const chunk = line.match(regex);
163 // Last match is always empty
164 chunk.pop();
165 return chunk || '';
166 });
167 }
168
169 forceLineReturn(content, width) {
170 width = width || this.normalizedCliWidth();
171 return _.flatten(this.breakLines(content.split('\n'), width)).join('\n');
172 }
173}
174
175module.exports = ScreenManager;