kodevu 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/progress-ui.js +18 -262
package/package.json
CHANGED
package/src/progress-ui.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
|
-
|
|
3
|
-
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
4
|
-
const ELLIPSIS = "...";
|
|
5
|
-
const MIN_DYNAMIC_WIDTH = 60;
|
|
6
|
-
|
|
7
1
|
function clampProgress(value) {
|
|
8
2
|
if (!Number.isFinite(value)) {
|
|
9
3
|
return 0;
|
|
@@ -12,78 +6,6 @@ function clampProgress(value) {
|
|
|
12
6
|
return Math.max(0, Math.min(1, value));
|
|
13
7
|
}
|
|
14
8
|
|
|
15
|
-
function getCharacterWidth(character) {
|
|
16
|
-
const codePoint = character.codePointAt(0);
|
|
17
|
-
|
|
18
|
-
if (!codePoint || codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
|
|
19
|
-
return 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
codePoint >= 0x1100 &&
|
|
24
|
-
(
|
|
25
|
-
codePoint <= 0x115f ||
|
|
26
|
-
codePoint === 0x2329 ||
|
|
27
|
-
codePoint === 0x232a ||
|
|
28
|
-
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
29
|
-
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
30
|
-
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
31
|
-
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
32
|
-
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
33
|
-
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
34
|
-
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
35
|
-
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
36
|
-
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
37
|
-
(codePoint >= 0x20000 && codePoint <= 0x3fffd)
|
|
38
|
-
)
|
|
39
|
-
) {
|
|
40
|
-
return 2;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getDisplayWidth(text) {
|
|
47
|
-
let width = 0;
|
|
48
|
-
|
|
49
|
-
for (const character of text) {
|
|
50
|
-
width += getCharacterWidth(character);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return width;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function truncateLine(line, maxWidth) {
|
|
57
|
-
if (!Number.isFinite(maxWidth) || maxWidth <= 0) {
|
|
58
|
-
return "";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (getDisplayWidth(line) <= maxWidth) {
|
|
62
|
-
return line;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (maxWidth <= 3) {
|
|
66
|
-
return ".".repeat(maxWidth);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const targetWidth = maxWidth - getDisplayWidth(ELLIPSIS);
|
|
70
|
-
let result = "";
|
|
71
|
-
let width = 0;
|
|
72
|
-
|
|
73
|
-
for (const character of line) {
|
|
74
|
-
const nextWidth = width + getCharacterWidth(character);
|
|
75
|
-
|
|
76
|
-
if (nextWidth > targetWidth) {
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
result += character;
|
|
81
|
-
width = nextWidth;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return `${result}${ELLIPSIS}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
9
|
class ProgressItem {
|
|
88
10
|
constructor(display, label) {
|
|
89
11
|
this.display = display;
|
|
@@ -91,20 +13,13 @@ class ProgressItem {
|
|
|
91
13
|
this.progress = 0;
|
|
92
14
|
this.stage = "";
|
|
93
15
|
this.active = false;
|
|
94
|
-
this.
|
|
16
|
+
this.lastStatusLine = "";
|
|
95
17
|
}
|
|
96
18
|
|
|
97
19
|
start(stage = "starting") {
|
|
98
20
|
this.active = true;
|
|
99
21
|
this.stage = stage;
|
|
100
|
-
|
|
101
|
-
if (!this.display.canRenderDynamically()) {
|
|
102
|
-
this.writeFallback(`... ${this.label}: ${stage}`);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.display.start();
|
|
107
|
-
this.display.activate(this);
|
|
22
|
+
this.writeStatus();
|
|
108
23
|
}
|
|
109
24
|
|
|
110
25
|
update(progress, stage) {
|
|
@@ -114,17 +29,11 @@ class ProgressItem {
|
|
|
114
29
|
this.stage = stage;
|
|
115
30
|
}
|
|
116
31
|
|
|
117
|
-
|
|
118
|
-
this.writeFallback(`... ${this.label}: ${this.stage}`);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this.display.start();
|
|
123
|
-
this.display.activate(this);
|
|
32
|
+
this.writeStatus();
|
|
124
33
|
}
|
|
125
34
|
|
|
126
35
|
log(message) {
|
|
127
|
-
this.display.
|
|
36
|
+
this.display.writeLine(message);
|
|
128
37
|
}
|
|
129
38
|
|
|
130
39
|
succeed(message) {
|
|
@@ -138,189 +47,36 @@ class ProgressItem {
|
|
|
138
47
|
finish(prefix, progress, message) {
|
|
139
48
|
this.progress = clampProgress(progress);
|
|
140
49
|
this.active = false;
|
|
141
|
-
this.display.
|
|
142
|
-
this.display.writeStaticLine(this.buildFinalLine(prefix, message));
|
|
50
|
+
this.display.writeLine(`${prefix} ${message}`);
|
|
143
51
|
}
|
|
144
52
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
53
|
+
writeStatus() {
|
|
54
|
+
const line = this.buildStatusLine();
|
|
148
55
|
|
|
149
|
-
|
|
150
|
-
return this.buildStatusLine(prefix, message);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
buildStatusLine(prefix, suffix) {
|
|
154
|
-
const availableWidth = this.display.getAvailableWidth();
|
|
155
|
-
const pct = `${Math.round(this.progress * 100)}`.padStart(3, " ");
|
|
156
|
-
const suffixText = suffix ? ` | ${suffix}` : "";
|
|
157
|
-
return truncateLine(`${prefix} ${pct}% ${this.label}${suffixText}`, availableWidth);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
writeFallback(line) {
|
|
161
|
-
if (line === this.lastFallbackLine) {
|
|
56
|
+
if (line === this.lastStatusLine) {
|
|
162
57
|
return;
|
|
163
58
|
}
|
|
164
59
|
|
|
165
|
-
this.
|
|
166
|
-
this.display.
|
|
60
|
+
this.lastStatusLine = line;
|
|
61
|
+
this.display.writeLine(line);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
buildStatusLine() {
|
|
65
|
+
const pct = `${Math.round(this.progress * 100)}`.padStart(3, " ");
|
|
66
|
+
return `[progress] ${pct}% ${this.label}${this.stage ? ` | ${this.stage}` : ""}`;
|
|
167
67
|
}
|
|
168
68
|
}
|
|
169
69
|
|
|
170
70
|
export class ProgressDisplay {
|
|
171
71
|
constructor(options = {}) {
|
|
172
72
|
this.stream = options.stream || process.stdout;
|
|
173
|
-
this.enabled = Boolean(this.stream.isTTY);
|
|
174
|
-
this.frameIndex = 0;
|
|
175
|
-
this.timer = null;
|
|
176
|
-
this.items = [];
|
|
177
|
-
this.currentItem = null;
|
|
178
|
-
this.statusVisible = false;
|
|
179
|
-
this.lastStatusWidth = 0;
|
|
180
|
-
this.resizeAttached = false;
|
|
181
|
-
this.handleResize = this.handleResize.bind(this);
|
|
182
73
|
}
|
|
183
74
|
|
|
184
75
|
createItem(label) {
|
|
185
|
-
|
|
186
|
-
this.items.push(item);
|
|
187
|
-
return item;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
start() {
|
|
191
|
-
if (!this.enabled || this.timer) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
this.attachResizeHandler();
|
|
196
|
-
this.timer = setInterval(() => {
|
|
197
|
-
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
198
|
-
this.render();
|
|
199
|
-
}, 120);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
activate(item) {
|
|
203
|
-
this.currentItem = item;
|
|
204
|
-
this.render();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
deactivate(item) {
|
|
208
|
-
if (this.currentItem === item) {
|
|
209
|
-
this.currentItem = this.items.findLast((candidate) => candidate.active) || null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
this.stopIfIdle();
|
|
213
|
-
this.clearStatusLine();
|
|
214
|
-
|
|
215
|
-
if (this.currentItem) {
|
|
216
|
-
this.render();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
log(message) {
|
|
221
|
-
if (!this.canRenderDynamically()) {
|
|
222
|
-
this.stream.write(`${message}\n`);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
this.clearStatusLine();
|
|
227
|
-
this.stream.write(`${truncateLine(message, this.getAvailableWidth())}\n`);
|
|
228
|
-
this.render();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
writeStaticLine(message) {
|
|
232
|
-
if (!this.canRenderDynamically()) {
|
|
233
|
-
this.stream.write(`${message}\n`);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
this.clearStatusLine();
|
|
238
|
-
this.stream.write(`${truncateLine(message, this.getAvailableWidth())}\n`);
|
|
239
|
-
this.render();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
render() {
|
|
243
|
-
if (!this.canRenderDynamically() || !this.currentItem?.active) {
|
|
244
|
-
this.clearStatusLine();
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
this.clearStatusLine();
|
|
249
|
-
const line = this.currentItem.renderLine(this.frameIndex);
|
|
250
|
-
this.stream.write(line);
|
|
251
|
-
this.statusVisible = true;
|
|
252
|
-
this.lastStatusWidth = getDisplayWidth(line);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
clearStatusLine() {
|
|
256
|
-
if (!this.enabled || !this.statusVisible) {
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const rows = Math.max(1, Math.ceil(this.lastStatusWidth / Math.max(this.stream.columns || 1, 1)));
|
|
261
|
-
|
|
262
|
-
readline.moveCursor(this.stream, 0, -Math.max(rows - 1, 0));
|
|
263
|
-
|
|
264
|
-
for (let index = 0; index < rows; index += 1) {
|
|
265
|
-
readline.clearLine(this.stream, 0);
|
|
266
|
-
readline.cursorTo(this.stream, 0);
|
|
267
|
-
|
|
268
|
-
if (index < rows - 1) {
|
|
269
|
-
readline.moveCursor(this.stream, 0, 1);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
readline.moveCursor(this.stream, 0, -Math.max(rows - 1, 0));
|
|
274
|
-
readline.cursorTo(this.stream, 0);
|
|
275
|
-
this.statusVisible = false;
|
|
276
|
-
this.lastStatusWidth = 0;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
stopIfIdle() {
|
|
280
|
-
if (this.items.some((item) => item.active)) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (this.timer) {
|
|
285
|
-
clearInterval(this.timer);
|
|
286
|
-
this.timer = null;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
this.detachResizeHandler();
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
getAvailableWidth() {
|
|
293
|
-
const columns = this.stream.columns || 80;
|
|
294
|
-
return Math.max(columns - 1, 1);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
canRenderDynamically() {
|
|
298
|
-
return this.enabled && this.getAvailableWidth() >= MIN_DYNAMIC_WIDTH;
|
|
76
|
+
return new ProgressItem(this, label);
|
|
299
77
|
}
|
|
300
78
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
this.render();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
attachResizeHandler() {
|
|
310
|
-
if (typeof this.stream.on !== "function" || this.resizeAttached) {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
this.stream.on("resize", this.handleResize);
|
|
315
|
-
this.resizeAttached = true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
detachResizeHandler() {
|
|
319
|
-
if (typeof this.stream.off !== "function" || !this.resizeAttached) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
this.stream.off("resize", this.handleResize);
|
|
324
|
-
this.resizeAttached = false;
|
|
79
|
+
writeLine(message) {
|
|
80
|
+
this.stream.write(`${message}\n`);
|
|
325
81
|
}
|
|
326
82
|
}
|