pixl-cli 1.1.0 → 1.1.2
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/README.md +101 -0
- package/cli.js +224 -16
- package/package.json +1 -1
- package/util.js +25 -1
- package/width.js +62 -1
- package/wrap.js +5 -24
- package/agents/string-width/.editorconfig +0 -12
- package/agents/string-width/.gitattributes +0 -1
- package/agents/string-width/.github/security.md +0 -3
- package/agents/string-width/.github/workflows/main.yml +0 -22
- package/agents/string-width/index.d.ts +0 -39
- package/agents/string-width/index.js +0 -207
- package/agents/string-width/index.test-d.ts +0 -7
- package/agents/string-width/license +0 -9
- package/agents/string-width/package.json +0 -65
- package/agents/string-width/readme.md +0 -66
- package/agents/string-width/test.js +0 -339
- package/agents/widest-line/.editorconfig +0 -12
- package/agents/widest-line/.gitattributes +0 -1
- package/agents/widest-line/.github/security.md +0 -3
- package/agents/widest-line/.github/workflows/main.yml +0 -21
- package/agents/widest-line/index.d.ts +0 -12
- package/agents/widest-line/index.js +0 -11
- package/agents/widest-line/license +0 -9
- package/agents/widest-line/package.json +0 -60
- package/agents/widest-line/readme.md +0 -26
- package/agents/widest-line/test.js +0 -8
- package/agents/word-wrap/.editorconfig +0 -13
- package/agents/word-wrap/.eslintrc.json +0 -122
- package/agents/word-wrap/.gitattributes +0 -10
- package/agents/word-wrap/.github/workflows/publish.yml +0 -19
- package/agents/word-wrap/.travis.yml +0 -13
- package/agents/word-wrap/.verb.md +0 -114
- package/agents/word-wrap/LICENSE +0 -21
- package/agents/word-wrap/README.md +0 -201
- package/agents/word-wrap/bower.json +0 -60
- package/agents/word-wrap/index.d.ts +0 -50
- package/agents/word-wrap/index.js +0 -61
- package/agents/word-wrap/package.json +0 -77
- package/agents/word-wrap/test.js +0 -69
- package/test.js +0 -20
package/README.md
CHANGED
|
@@ -398,6 +398,105 @@ culpa qui officia deserunt mollit anim
|
|
|
398
398
|
id est laborum.
|
|
399
399
|
```
|
|
400
400
|
|
|
401
|
+
## Displaying Definition Lists
|
|
402
|
+
|
|
403
|
+
Call `cli.defList()` to render a two-column list of labels and values inside a box. Labels are bold by default, values are plain, and no vertical border is drawn between the columns. Colons are added to labels automatically when needed. Lines will automatically truncate with ellipsis as needed.
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
const cli = require('pixl-cli');
|
|
407
|
+
|
|
408
|
+
let event = {
|
|
409
|
+
id: "emm2wtlsxrtjz8v4",
|
|
410
|
+
title: "Simple Shell Test",
|
|
411
|
+
category: "general",
|
|
412
|
+
username: "admin"
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
cli.println( cli.defList([
|
|
416
|
+
[ "Event ID", event.id ],
|
|
417
|
+
[ "Title", event.title ],
|
|
418
|
+
[ "Category", event.category ],
|
|
419
|
+
[ "Author", event.username ]
|
|
420
|
+
], {
|
|
421
|
+
labelStyles: ["yellow", "bold"],
|
|
422
|
+
textStyles: ["green"],
|
|
423
|
+
indent: 1
|
|
424
|
+
}) );
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
This produces a definition list like this, with colors when the terminal supports them:
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
┌─────────────────────────────┐
|
|
431
|
+
│ Event ID: emm2wtlsxrtjz8v4 │
|
|
432
|
+
│ Title: Simple Shell Test │
|
|
433
|
+
│ Category: general │
|
|
434
|
+
│ Author: admin │
|
|
435
|
+
└─────────────────────────────┘
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
The label and value columns are left-aligned. When the box would exceed the attached terminal width, long values are shortened with an ellipsis. The complete label column is preserved whenever possible. The `indent` is treated as a horizontal margin on both sides when calculating the available width.
|
|
439
|
+
|
|
440
|
+
You can customize the definition list with these options:
|
|
441
|
+
|
|
442
|
+
| Property Name | Description |
|
|
443
|
+
|---------------|-------------|
|
|
444
|
+
| `labelStyles` | An array of [chalk](https://www.npmjs.com/package/chalk) styles or functions for labels. Defaults to `["bold"]`. |
|
|
445
|
+
| `textStyles` | An array of styles or functions for values. Defaults to `[]`. |
|
|
446
|
+
| `borderStyles` | An array of styles or functions for the box border. Defaults to `["gray"]`. |
|
|
447
|
+
| `indent` | Horizontal margin in characters on both sides of the box. Defaults to `0`. |
|
|
448
|
+
| `gap` | Spaces between the label and value columns. Defaults to `1`. |
|
|
449
|
+
| `hspace` | Spaces between the content and each side of the box. Defaults to `1`. |
|
|
450
|
+
| `vspace` | Empty lines above and below the list inside the box. Defaults to `0`. |
|
|
451
|
+
|
|
452
|
+
## Displaying Dashboard Grids
|
|
453
|
+
|
|
454
|
+
Call `cli.dashGrid()` to render a responsive grid of dashboard units. Each unit contains a centered value and label, and all units have the same dimensions regardless of their content. Pass an array of `[label, value]` pairs:
|
|
455
|
+
|
|
456
|
+
```js
|
|
457
|
+
const cli = require('pixl-cli');
|
|
458
|
+
|
|
459
|
+
cli.println( cli.dashGrid([
|
|
460
|
+
[ "Conductors", 1 ],
|
|
461
|
+
[ "Servers", 2 ],
|
|
462
|
+
[ "Current Alerts", 0 ],
|
|
463
|
+
[ "Active Jobs", 0 ],
|
|
464
|
+
[ "Job Success Rate", "100%" ],
|
|
465
|
+
[ "Avg. Job Elapsed", "31 sec" ]
|
|
466
|
+
], {
|
|
467
|
+
minCols: 3,
|
|
468
|
+
maxCols: 5,
|
|
469
|
+
gap: 1,
|
|
470
|
+
valueStyles: ["bold", "green"]
|
|
471
|
+
}) );
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
At a width of 62 columns, the first row looks like this:
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
|
478
|
+
│ 1 │ │ 2 │ │ 0 │
|
|
479
|
+
│ │ │ │ │ │
|
|
480
|
+
│ Conductors │ │ Servers │ │ Current Alerts │
|
|
481
|
+
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The grid uses the current terminal width and flows additional units onto new rows. An incomplete final row ends after its last unit. Labels and values are limited to one line and automatically shortened with an ellipsis when necessary. ANSI styles and emoji are measured by their terminal display width.
|
|
485
|
+
|
|
486
|
+
You can customize the dashboard grid with these options:
|
|
487
|
+
|
|
488
|
+
| Property Name | Description |
|
|
489
|
+
|---------------|-------------|
|
|
490
|
+
| `width` | Overall layout width, including horizontal margins. Defaults to `cli.width()`, or `80` when no terminal width is available. |
|
|
491
|
+
| `unitWidth` | Target outer width used to select the responsive column count. Defaults to `20`. |
|
|
492
|
+
| `minCols` | Preferred minimum number of columns. Defaults to `3`. Extremely narrow terminals may use fewer to avoid overflow. |
|
|
493
|
+
| `maxCols` | Maximum number of columns. Defaults to `5`. |
|
|
494
|
+
| `gap` | Horizontal spaces between units and blank lines between grid rows. Defaults to `1`. |
|
|
495
|
+
| `indent` | Horizontal margin in characters on both sides of the grid. Defaults to `0`. |
|
|
496
|
+
| `valueStyles` | An array of [chalk](https://www.npmjs.com/package/chalk) styles or functions for values. Defaults to `["bold"]`. |
|
|
497
|
+
| `labelStyles` | An array of styles or functions for labels. Defaults to `["gray"]`. |
|
|
498
|
+
| `borderStyles` | An array of styles or functions for unit borders. Defaults to `["gray"]`. |
|
|
499
|
+
|
|
401
500
|
## Displaying Tables
|
|
402
501
|
|
|
403
502
|

|
|
@@ -687,6 +786,8 @@ The full list of methods and objects that are imported are:
|
|
|
687
786
|
- `yesno()`
|
|
688
787
|
- `table()`
|
|
689
788
|
- `box()`
|
|
789
|
+
- `defList()`
|
|
790
|
+
- `dashGrid()`
|
|
690
791
|
- `wrap()`
|
|
691
792
|
- `center()`
|
|
692
793
|
- `commify()`
|
package/cli.js
CHANGED
|
@@ -197,6 +197,227 @@ var cli = module.exports = {
|
|
|
197
197
|
return output.join("\n");
|
|
198
198
|
},
|
|
199
199
|
|
|
200
|
+
defList: function(rows, args) {
|
|
201
|
+
// Render a two-column definition list inside a box. The labels share a
|
|
202
|
+
// common width, but there is deliberately no border between the columns.
|
|
203
|
+
var self = this;
|
|
204
|
+
if (!args) args = {};
|
|
205
|
+
|
|
206
|
+
// Empty arrays are valid style overrides, so test for the property rather
|
|
207
|
+
// than using || here. Labels are bold by default and values are plain.
|
|
208
|
+
var labelStyles = ('labelStyles' in args) ? args.labelStyles : ["bold"];
|
|
209
|
+
var textStyles = ('textStyles' in args) ? args.textStyles : [];
|
|
210
|
+
var borderStyles = ('borderStyles' in args) ? args.borderStyles :
|
|
211
|
+
(('styles' in args) ? args.styles : ["gray"]);
|
|
212
|
+
var hspace = ('hspace' in args) ? args.hspace : 1;
|
|
213
|
+
var vspace = args.vspace || 0;
|
|
214
|
+
|
|
215
|
+
// Gaps are whole terminal cells. Invalid and negative values become zero.
|
|
216
|
+
var gap = ('gap' in args) ? Math.floor(Number(args.gap)) : 1;
|
|
217
|
+
if (!isFinite(gap) || (gap < 0)) gap = 0;
|
|
218
|
+
var indent = args.indent || "";
|
|
219
|
+
if (typeof(indent) == 'number') indent = this.space(indent);
|
|
220
|
+
|
|
221
|
+
// Normalize and style all cells before measuring them. This allows ANSI
|
|
222
|
+
// color, custom style functions and the cli.emoji() cursor hack to coexist
|
|
223
|
+
// with the display-width calculations below.
|
|
224
|
+
var items = (rows || []).map( function(row) {
|
|
225
|
+
var label = '' + row[0];
|
|
226
|
+
var value = '' + row[1];
|
|
227
|
+
|
|
228
|
+
// Definition lists are one item per line. Flatten accidental newlines so
|
|
229
|
+
// a cell cannot escape its row and disturb the surrounding box.
|
|
230
|
+
label = label.replace(/\r?\n/g, ' ').replace(/\s+$/, '');
|
|
231
|
+
value = value.replace(/\r?\n/g, ' ');
|
|
232
|
+
if (!label.replace(ansiPattern, '').match(/:$/)) label += ':';
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
label: self.applyStyles(label, labelStyles),
|
|
236
|
+
value: self.applyStyles(value, textStyles)
|
|
237
|
+
};
|
|
238
|
+
} );
|
|
239
|
+
|
|
240
|
+
var labelWidth = 0;
|
|
241
|
+
var valueWidth = 0;
|
|
242
|
+
|
|
243
|
+
items.forEach( function(item) {
|
|
244
|
+
labelWidth = Math.max( labelWidth, stringWidth(item.label) );
|
|
245
|
+
valueWidth = Math.max( valueWidth, stringWidth(item.value) );
|
|
246
|
+
} );
|
|
247
|
+
|
|
248
|
+
// The indent acts as a horizontal margin, so reserve it on both sides.
|
|
249
|
+
// Also reserve both box borders, both padding areas, and the requested gap
|
|
250
|
+
// separating the label and value columns.
|
|
251
|
+
var terminalWidth = this.width();
|
|
252
|
+
var contentWidth = labelWidth + gap + valueWidth;
|
|
253
|
+
|
|
254
|
+
if (terminalWidth) {
|
|
255
|
+
var availableWidth = terminalWidth - (stringWidth(indent) * 2);
|
|
256
|
+
var availableContentWidth = Math.max(0, availableWidth - 2 - (hspace * 2));
|
|
257
|
+
contentWidth = Math.min(contentWidth, availableContentWidth);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Keep the complete label column whenever possible, and give all remaining
|
|
261
|
+
// room to values. On extremely narrow terminals labels are shortened too,
|
|
262
|
+
// while still reserving one cell for a value when any room remains.
|
|
263
|
+
if (labelWidth + gap + 1 > contentWidth) {
|
|
264
|
+
labelWidth = Math.max(0, contentWidth - gap - 1);
|
|
265
|
+
}
|
|
266
|
+
valueWidth = Math.max(0, contentWidth - labelWidth - (labelWidth ? gap : 0));
|
|
267
|
+
|
|
268
|
+
var truncate = function(text, width) {
|
|
269
|
+
// Width.truncate() preserves ANSI resets and complete emoji graphemes.
|
|
270
|
+
if (width < 1) return '';
|
|
271
|
+
return (stringWidth(text) > width) ? Width.truncate(text, width, '…') : text;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
var lines = items.map( function(item) {
|
|
275
|
+
var label = truncate(item.label, labelWidth);
|
|
276
|
+
var value = truncate(item.value, valueWidth);
|
|
277
|
+
if (!labelWidth) return value;
|
|
278
|
+
return self.pad(label, labelWidth) + self.space(gap) + value;
|
|
279
|
+
} );
|
|
280
|
+
|
|
281
|
+
// Let box() handle the border, padding, vertical spacing and final row
|
|
282
|
+
// padding. Passing the normalized indent keeps numeric margins consistent.
|
|
283
|
+
return this.box( lines.join("\n"), {
|
|
284
|
+
styles: borderStyles,
|
|
285
|
+
hspace: hspace,
|
|
286
|
+
vspace: vspace,
|
|
287
|
+
indent: indent
|
|
288
|
+
} );
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
dashGrid: function(rows, args) {
|
|
292
|
+
// Render a responsive grid of equal-sized dashboard units. Each unit has a
|
|
293
|
+
// centered value, a centered label, two blank spacer rows and its own border.
|
|
294
|
+
var self = this;
|
|
295
|
+
if (!rows || !rows.length) return '';
|
|
296
|
+
if (!args) args = {};
|
|
297
|
+
|
|
298
|
+
// Normalize numeric layout options to non-negative whole terminal cells.
|
|
299
|
+
// Invalid values fall back to their documented defaults.
|
|
300
|
+
var wholeNumber = function(value, defaultValue, minimum) {
|
|
301
|
+
value = Math.floor(Number(value));
|
|
302
|
+
if (!isFinite(value)) value = defaultValue;
|
|
303
|
+
return Math.max(minimum, value);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
var targetUnitWidth = wholeNumber(args.unitWidth, 20, 5);
|
|
307
|
+
var minCols = wholeNumber(args.minCols, 3, 1);
|
|
308
|
+
var maxCols = wholeNumber(args.maxCols, 5, 1);
|
|
309
|
+
var gap = wholeNumber(args.gap, 1, 0);
|
|
310
|
+
var indent = args.indent || '';
|
|
311
|
+
if (typeof(indent) == 'number') indent = this.space(indent);
|
|
312
|
+
|
|
313
|
+
// A maximum is a hard cap. If conflicting limits are supplied, lower the
|
|
314
|
+
// minimum to match rather than silently exceeding the requested maximum.
|
|
315
|
+
if (minCols > maxCols) minCols = maxCols;
|
|
316
|
+
|
|
317
|
+
// Empty style arrays are intentional overrides, so property checks are used
|
|
318
|
+
// instead of ||. Values are bold, labels and borders are gray by default.
|
|
319
|
+
var valueStyles = ('valueStyles' in args) ? args.valueStyles : ["bold"];
|
|
320
|
+
var labelStyles = ('labelStyles' in args) ? args.labelStyles : ["gray"];
|
|
321
|
+
var borderStyles = ('borderStyles' in args) ? args.borderStyles : ["gray"];
|
|
322
|
+
|
|
323
|
+
// An explicit width makes redirected output and tests deterministic. When
|
|
324
|
+
// attached to a terminal, cli.width() returns process.stdout.columns.
|
|
325
|
+
var layoutWidth = ('width' in args) ?
|
|
326
|
+
wholeNumber(args.width, 80, 0) : (this.width() || 80);
|
|
327
|
+
var availableWidth = Math.max(0, layoutWidth - (stringWidth(indent) * 2));
|
|
328
|
+
if (availableWidth < 2) return '';
|
|
329
|
+
|
|
330
|
+
// Start with the preferred responsive column count, bounded by the caller's
|
|
331
|
+
// limits and the number of units. unitWidth is a planning target; the actual
|
|
332
|
+
// width is calculated below after horizontal gaps have been reserved.
|
|
333
|
+
var numCols = Math.floor(availableWidth / targetUnitWidth);
|
|
334
|
+
numCols = Math.max(minCols, Math.min(maxCols, numCols));
|
|
335
|
+
numCols = Math.min(numCols, rows.length);
|
|
336
|
+
|
|
337
|
+
// Each useful unit needs five cells: two borders, two padding cells and one
|
|
338
|
+
// content cell. On a very narrow terminal, safety takes priority over the
|
|
339
|
+
// requested minimum column count so the grid never overflows.
|
|
340
|
+
var maxFittingCols = Math.floor((availableWidth + gap) / (5 + gap));
|
|
341
|
+
maxFittingCols = Math.max(1, maxFittingCols);
|
|
342
|
+
numCols = Math.max(1, Math.min(numCols, maxFittingCols));
|
|
343
|
+
|
|
344
|
+
// All units use the same exact outer width. Any indivisible remainder stays
|
|
345
|
+
// unused at the right edge, including on incomplete final rows.
|
|
346
|
+
var unitWidth = Math.floor(
|
|
347
|
+
(availableWidth - ((numCols - 1) * gap)) / numCols
|
|
348
|
+
);
|
|
349
|
+
if (unitWidth < 2) return '';
|
|
350
|
+
|
|
351
|
+
var innerWidth = unitWidth - 2;
|
|
352
|
+
var contentWidth = Math.max(0, innerWidth - 2);
|
|
353
|
+
|
|
354
|
+
var truncate = function(text) {
|
|
355
|
+
// Preserve ANSI styles, complete graphemes and cli.emoji() sequences.
|
|
356
|
+
if (contentWidth < 1) return '';
|
|
357
|
+
return (stringWidth(text) > contentWidth) ?
|
|
358
|
+
Width.truncate(text, contentWidth, '…') : text;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
var centerCell = function(text) {
|
|
362
|
+
// Center by terminal display width rather than JavaScript string length.
|
|
363
|
+
var remain = Math.max(0, innerWidth - stringWidth(text));
|
|
364
|
+
var left = Math.floor(remain / 2);
|
|
365
|
+
return self.space(left) + text + self.space(remain - left);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
var units = rows.map( function(row) {
|
|
369
|
+
var label = ('' + row[0]).replace(/\r?\n/g, ' ');
|
|
370
|
+
var value = ('' + row[1]).replace(/\r?\n/g, ' ');
|
|
371
|
+
return {
|
|
372
|
+
label: self.applyStyles(label, labelStyles),
|
|
373
|
+
value: self.applyStyles(value, valueStyles)
|
|
374
|
+
};
|
|
375
|
+
} );
|
|
376
|
+
|
|
377
|
+
var renderUnit = function(unit) {
|
|
378
|
+
// The interior layout is: blank, value, blank, label.
|
|
379
|
+
var top = self.applyStyles(
|
|
380
|
+
'┌' + self.repeat('─', innerWidth) + '┐', borderStyles
|
|
381
|
+
);
|
|
382
|
+
var bottom = self.applyStyles(
|
|
383
|
+
'└' + self.repeat('─', innerWidth) + '┘', borderStyles
|
|
384
|
+
);
|
|
385
|
+
var leftBorder = self.applyStyles('│', borderStyles);
|
|
386
|
+
var rightBorder = self.applyStyles('│', borderStyles);
|
|
387
|
+
var blank = leftBorder + self.space(innerWidth) + rightBorder;
|
|
388
|
+
|
|
389
|
+
return [
|
|
390
|
+
top,
|
|
391
|
+
// blank,
|
|
392
|
+
leftBorder + centerCell(truncate(unit.value)) + rightBorder,
|
|
393
|
+
blank,
|
|
394
|
+
leftBorder + centerCell(truncate(unit.label)) + rightBorder,
|
|
395
|
+
bottom
|
|
396
|
+
];
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
var output = [];
|
|
400
|
+
for (var rowIdx = 0; rowIdx < units.length; rowIdx += numCols) {
|
|
401
|
+
var gridRow = units.slice(rowIdx, rowIdx + numCols).map(renderUnit);
|
|
402
|
+
|
|
403
|
+
// Join corresponding lines from each unit to form one complete grid row.
|
|
404
|
+
for (var lineIdx = 0; lineIdx < 6; lineIdx++) {
|
|
405
|
+
output.push(
|
|
406
|
+
indent + gridRow.map( function(unit) {
|
|
407
|
+
return unit[lineIdx];
|
|
408
|
+
} ).join(self.space(gap))
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// The same gap controls vertical blank lines between rows of units.
|
|
413
|
+
if (rowIdx + numCols < units.length) {
|
|
414
|
+
for (var gapIdx = 0; gapIdx < gap; gapIdx++) output.push('');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return output.join("\n");
|
|
419
|
+
},
|
|
420
|
+
|
|
200
421
|
applyStyles: function(text, styles) {
|
|
201
422
|
// apply one or more chalk styles or functions to text string
|
|
202
423
|
if (!styles) return text;
|
|
@@ -298,25 +519,12 @@ var cli = module.exports = {
|
|
|
298
519
|
max_col_widths[longest_col_idx]--;
|
|
299
520
|
}
|
|
300
521
|
|
|
301
|
-
// finally prune affected columns
|
|
522
|
+
// finally prune affected columns by display width, preserving ANSI and emoji
|
|
302
523
|
rows.forEach( function(cols, idx) {
|
|
303
524
|
cols.forEach( function(col, idy) {
|
|
304
525
|
col = '' + col;
|
|
305
526
|
if (stringWidth(col) > max_col_widths[idy]) {
|
|
306
|
-
|
|
307
|
-
var prefix = '';
|
|
308
|
-
|
|
309
|
-
while (col.match(/^(\u001b\[[^m]*?m)/)) {
|
|
310
|
-
prefix += RegExp.$1;
|
|
311
|
-
col = col.replace(/^(\u001b\[[^m]*?m)/, '');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
while (col.match(/(\u001b\[[^m]*?m)$/)) {
|
|
315
|
-
suffix = RegExp.$1 + suffix;
|
|
316
|
-
col = col.replace(/(\u001b\[[^m]*?m)$/, '');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
cols[idy] = prefix + col.substring(0, max_col_widths[idy] - 1) + '…' + suffix;
|
|
527
|
+
cols[idy] = Width.truncate(col, max_col_widths[idy], '…');
|
|
320
528
|
} // too wide
|
|
321
529
|
});
|
|
322
530
|
});
|
|
@@ -500,7 +708,7 @@ var cli = module.exports = {
|
|
|
500
708
|
global.Tools = Tools;
|
|
501
709
|
|
|
502
710
|
// bind wrap functions
|
|
503
|
-
["prompt", "yesno", "table", "box", "wrap", "center", "print", "println", "verbose", "verboseln", "warn", "warnln", "die", "dieln", "loadFile", "saveFile", "appendFile"].forEach( function(func) {
|
|
711
|
+
["prompt", "yesno", "table", "box", "defList", "dashGrid", "wrap", "center", "print", "println", "verbose", "verboseln", "warn", "warnln", "die", "dieln", "loadFile", "saveFile", "appendFile"].forEach( function(func) {
|
|
504
712
|
global[func] = self[func].bind(self);
|
|
505
713
|
} );
|
|
506
714
|
|
package/package.json
CHANGED
package/util.js
CHANGED
|
@@ -9,7 +9,31 @@ var ansiPattern = new RegExp([
|
|
|
9
9
|
// Split strings into user-perceived characters, including complete emoji sequences.
|
|
10
10
|
var graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
11
11
|
|
|
12
|
+
function splitAnsiGraphemes(text) {
|
|
13
|
+
// Split a string into complete ANSI sequences and Unicode grapheme clusters.
|
|
14
|
+
// ANSI sequences are kept intact so callers can preserve them while editing text.
|
|
15
|
+
var units = [];
|
|
16
|
+
var pattern = new RegExp(ansiPattern.source, 'g');
|
|
17
|
+
var offset = 0;
|
|
18
|
+
var match = null;
|
|
19
|
+
|
|
20
|
+
var addText = function(value) {
|
|
21
|
+
for (var item of graphemeSegmenter.segment(value)) {
|
|
22
|
+
units.push({ text: item.segment, ansi: false });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
while ((match = pattern.exec(text))) {
|
|
27
|
+
if (match.index > offset) addText( text.substring(offset, match.index) );
|
|
28
|
+
units.push({ text: match[0], ansi: true });
|
|
29
|
+
offset = pattern.lastIndex;
|
|
30
|
+
}
|
|
31
|
+
if (offset < text.length) addText( text.substring(offset) );
|
|
32
|
+
return units;
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
module.exports = {
|
|
13
36
|
ansiPattern: ansiPattern,
|
|
14
|
-
graphemeSegmenter: graphemeSegmenter
|
|
37
|
+
graphemeSegmenter: graphemeSegmenter,
|
|
38
|
+
splitAnsiGraphemes: splitAnsiGraphemes
|
|
15
39
|
};
|
package/width.js
CHANGED
|
@@ -65,11 +65,72 @@ function widestLine(text) {
|
|
|
65
65
|
return width;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function truncate(text, width, suffix) {
|
|
69
|
+
// Truncate a string to an exact terminal display width. Preserve complete ANSI
|
|
70
|
+
// sequences and grapheme clusters, and retain trailing SGR codes to reset styles.
|
|
71
|
+
if (typeof(text) != 'string') text = '' + text;
|
|
72
|
+
if (suffix == null) suffix = '';
|
|
73
|
+
if (stringWidth(text) <= width) return text;
|
|
74
|
+
|
|
75
|
+
var suffixWidth = stringWidth(suffix);
|
|
76
|
+
var contentWidth = Math.max(0, width - suffixWidth);
|
|
77
|
+
var outputWidth = 0;
|
|
78
|
+
var output = '';
|
|
79
|
+
var units = Util.splitAnsiGraphemes(text);
|
|
80
|
+
var trailingAnsi = [];
|
|
81
|
+
|
|
82
|
+
// cli.emoji() surrounds one grapheme with cursor save, restore and movement
|
|
83
|
+
// sequences. Treat this entire construct as one display unit so truncation
|
|
84
|
+
// can never keep the save sequence while discarding its matching restore.
|
|
85
|
+
for (var idx = 0; idx <= units.length - 4; idx++) {
|
|
86
|
+
if (
|
|
87
|
+
units[idx].ansi && (units[idx].text === '\u001b7') &&
|
|
88
|
+
!units[idx + 1].ansi &&
|
|
89
|
+
units[idx + 2].ansi && (units[idx + 2].text === '\u001b8') &&
|
|
90
|
+
units[idx + 3].ansi && (units[idx + 3].text === '\u001b[2C')
|
|
91
|
+
) {
|
|
92
|
+
var emojiUnit = units.slice(idx, idx + 4).map( function(unit) {
|
|
93
|
+
return unit.text;
|
|
94
|
+
} ).join('');
|
|
95
|
+
units.splice(idx, 4, {
|
|
96
|
+
text: emojiUnit,
|
|
97
|
+
ansi: false
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (var idx = units.length - 1; idx >= 0; idx--) {
|
|
103
|
+
if (!units[idx].ansi) break;
|
|
104
|
+
if (units[idx].text.match(/^(?:\u001B\[|\u009B)[^m]*m$/)) {
|
|
105
|
+
trailingAnsi.unshift(units[idx].text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (var idx = 0, len = units.length; idx < len; idx++) {
|
|
110
|
+
var unit = units[idx];
|
|
111
|
+
if (unit.ansi) {
|
|
112
|
+
output += unit.text;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var unitWidth = stringWidth(unit.text);
|
|
117
|
+
if ((outputWidth + unitWidth) > contentWidth) break;
|
|
118
|
+
output += unit.text;
|
|
119
|
+
outputWidth += unitWidth;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A wide grapheme may not fit the final available cell. Pad that cell so the
|
|
123
|
+
// ellipsis still lands at the exact requested width and tables remain aligned.
|
|
124
|
+
if (outputWidth < contentWidth) output += ' '.repeat(contentWidth - outputWidth);
|
|
125
|
+
return output + suffix + trailingAnsi.join('');
|
|
126
|
+
}
|
|
127
|
+
|
|
68
128
|
// Preserve the compatibility aliases provided by the old CommonJS dependencies.
|
|
69
129
|
stringWidth.default = stringWidth;
|
|
70
130
|
widestLine.default = widestLine;
|
|
71
131
|
|
|
72
132
|
module.exports = {
|
|
73
133
|
stringWidth: stringWidth,
|
|
74
|
-
widestLine: widestLine
|
|
134
|
+
widestLine: widestLine,
|
|
135
|
+
truncate: truncate
|
|
75
136
|
};
|
package/wrap.js
CHANGED
|
@@ -2,34 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
var Util = require('./util');
|
|
4
4
|
var stringWidth = require('./width').stringWidth;
|
|
5
|
-
var ansiPattern = Util.ansiPattern;
|
|
6
|
-
var graphemeSegmenter = Util.graphemeSegmenter;
|
|
7
5
|
|
|
8
6
|
function getWrapTokens(text) {
|
|
9
7
|
// Split text into alternating words and breakable whitespace, while preserving
|
|
10
8
|
// ANSI sequences as zero-width units in their original positions.
|
|
11
|
-
var units =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
var addText = function(value) {
|
|
17
|
-
for (var item of graphemeSegmenter.segment(value)) {
|
|
18
|
-
units.push({
|
|
19
|
-
text: item.segment,
|
|
20
|
-
width: stringWidth(item.segment),
|
|
21
|
-
breakable: !!item.segment.match(/^(?:\s|\u200B)+$/u),
|
|
22
|
-
ansi: false
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
while ((match = pattern.exec(text))) {
|
|
28
|
-
if (match.index > offset) addText( text.substring(offset, match.index) );
|
|
29
|
-
units.push({ text: match[0], width: 0, breakable: false, ansi: true });
|
|
30
|
-
offset = pattern.lastIndex;
|
|
31
|
-
}
|
|
32
|
-
if (offset < text.length) addText( text.substring(offset) );
|
|
9
|
+
var units = Util.splitAnsiGraphemes(text);
|
|
10
|
+
units.forEach( function(unit) {
|
|
11
|
+
unit.width = unit.ansi ? 0 : stringWidth(unit.text);
|
|
12
|
+
unit.breakable = !unit.ansi && !!unit.text.match(/^(?:\s|\u200B)+$/u);
|
|
13
|
+
} );
|
|
33
14
|
|
|
34
15
|
var tokens = [];
|
|
35
16
|
var token = null;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* text=auto eol=lf
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
on:
|
|
3
|
-
- push
|
|
4
|
-
- pull_request
|
|
5
|
-
jobs:
|
|
6
|
-
test:
|
|
7
|
-
name: Node.js ${{ matrix.node-version }}
|
|
8
|
-
runs-on: ubuntu-latest
|
|
9
|
-
strategy:
|
|
10
|
-
fail-fast: false
|
|
11
|
-
matrix:
|
|
12
|
-
node-version:
|
|
13
|
-
- 24
|
|
14
|
-
- 22
|
|
15
|
-
- 20
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v4
|
|
18
|
-
- uses: actions/setup-node@v4
|
|
19
|
-
with:
|
|
20
|
-
node-version: ${{ matrix.node-version }}
|
|
21
|
-
- run: npm install
|
|
22
|
-
- run: npm test
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export type Options = {
|
|
2
|
-
/**
|
|
3
|
-
Count [ambiguous width characters](https://www.unicode.org/reports/tr11/#Ambiguous) as having narrow width (count of 1) instead of wide width (count of 2).
|
|
4
|
-
|
|
5
|
-
@default true
|
|
6
|
-
|
|
7
|
-
> Ambiguous characters behave like wide or narrow characters depending on the context (language tag, script identification, associated font, source of data, or explicit markup; all can provide the context). __If the context cannot be established reliably, they should be treated as narrow characters by default.__
|
|
8
|
-
> - http://www.unicode.org/reports/tr11/
|
|
9
|
-
*/
|
|
10
|
-
readonly ambiguousIsNarrow?: boolean;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
Whether [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) should be counted.
|
|
14
|
-
|
|
15
|
-
@default false
|
|
16
|
-
*/
|
|
17
|
-
readonly countAnsiEscapeCodes?: boolean;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
Get the visual width of a string - the number of columns required to display it.
|
|
22
|
-
|
|
23
|
-
Some Unicode characters are [fullwidth](https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms) and use double the normal width. [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) are stripped and doesn't affect the width.
|
|
24
|
-
|
|
25
|
-
@example
|
|
26
|
-
```
|
|
27
|
-
import stringWidth from 'string-width';
|
|
28
|
-
|
|
29
|
-
stringWidth('a');
|
|
30
|
-
//=> 1
|
|
31
|
-
|
|
32
|
-
stringWidth('古');
|
|
33
|
-
//=> 2
|
|
34
|
-
|
|
35
|
-
stringWidth('\u001B[1m古\u001B[22m');
|
|
36
|
-
//=> 2
|
|
37
|
-
```
|
|
38
|
-
*/
|
|
39
|
-
export default function stringWidth(string: string, options?: Options): number;
|