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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/progress-ui.js +18 -262
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
6
6
  "bin": {
@@ -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.lastFallbackLine = "";
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
- if (!this.display.canRenderDynamically()) {
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.log(message);
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.deactivate(this);
142
- this.display.writeStaticLine(this.buildFinalLine(prefix, message));
50
+ this.display.writeLine(`${prefix} ${message}`);
143
51
  }
144
52
 
145
- renderLine(frameIndex) {
146
- return this.buildStatusLine(SPINNER_FRAMES[frameIndex], this.stage);
147
- }
53
+ writeStatus() {
54
+ const line = this.buildStatusLine();
148
55
 
149
- buildFinalLine(prefix, message) {
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.lastFallbackLine = line;
166
- this.display.stream.write(`${line}\n`);
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
- const item = new ProgressItem(this, label);
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
- handleResize() {
302
- if (!this.enabled) {
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
  }