console-toolkit 1.2.14 → 1.3.0

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 (88) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +4 -0
  3. package/llms-full.txt +64 -4
  4. package/package.json +6 -6
  5. package/src/alphanumeric/arrows.js +0 -23
  6. package/src/alphanumeric/fractions.js +0 -49
  7. package/src/alphanumeric/number-formatters.js +0 -44
  8. package/src/alphanumeric/roman.js +0 -12
  9. package/src/alphanumeric/unicode-cultural-numbers.js +0 -1
  10. package/src/alphanumeric/unicode-letters.js +0 -8
  11. package/src/alphanumeric/unicode-numbers.js +0 -21
  12. package/src/alphanumeric/utils.js +0 -26
  13. package/src/ansi/csi.js +0 -49
  14. package/src/ansi/sgr-state.js +7 -50
  15. package/src/ansi/sgr.js +0 -420
  16. package/src/box.d.ts +4 -1
  17. package/src/box.js +15 -115
  18. package/src/charts/bars/block-frac-grouped.js +0 -6
  19. package/src/charts/bars/block-frac.js +1 -14
  20. package/src/charts/bars/block-grouped.js +0 -6
  21. package/src/charts/bars/block.js +1 -14
  22. package/src/charts/bars/draw-grouped.js +0 -4
  23. package/src/charts/bars/draw-stacked.js +0 -4
  24. package/src/charts/bars/frac-grouped.js +2 -14
  25. package/src/charts/bars/plain-grouped.js +0 -6
  26. package/src/charts/bars/plain.js +1 -28
  27. package/src/charts/columns/block-frac-grouped.js +0 -6
  28. package/src/charts/columns/block-frac.js +0 -13
  29. package/src/charts/columns/block-grouped.js +0 -6
  30. package/src/charts/columns/block.js +0 -13
  31. package/src/charts/columns/draw-grouped.js +1 -5
  32. package/src/charts/columns/draw-stacked.js +0 -4
  33. package/src/charts/columns/frac-grouped.js +1 -13
  34. package/src/charts/columns/plain-grouped.js +0 -6
  35. package/src/charts/columns/plain.js +0 -13
  36. package/src/charts/themes/default.js +0 -1
  37. package/src/charts/themes/rainbow-reversed.js +0 -1
  38. package/src/charts/themes/rainbow.js +0 -1
  39. package/src/charts/utils.js +2 -28
  40. package/src/draw-block-frac.js +0 -14
  41. package/src/draw-block.js +0 -24
  42. package/src/meta.js +0 -64
  43. package/src/output/show.d.ts +8 -3
  44. package/src/output/show.js +7 -38
  45. package/src/output/updater.d.ts +8 -3
  46. package/src/output/updater.js +4 -51
  47. package/src/output/writer.js +1 -53
  48. package/src/panel.d.ts +16 -10
  49. package/src/panel.js +21 -276
  50. package/src/plot/bitmap.js +5 -33
  51. package/src/plot/draw-line.js +0 -8
  52. package/src/plot/draw-rect.js +25 -103
  53. package/src/plot/index.js +0 -22
  54. package/src/plot/to-quads.js +3 -6
  55. package/src/spinner/spin.js +23 -20
  56. package/src/spinner/spinner.d.ts +16 -2
  57. package/src/spinner/spinner.js +22 -34
  58. package/src/spinner/spinners.js +0 -16
  59. package/src/strings/clip.js +0 -10
  60. package/src/strings/parse.js +0 -7
  61. package/src/strings/split.js +0 -15
  62. package/src/strings.d.ts +2 -0
  63. package/src/strings.js +2 -32
  64. package/src/style.js +2 -58
  65. package/src/symbols.js +0 -84
  66. package/src/table/draw-borders.js +0 -9
  67. package/src/table/index.d.ts +1 -1
  68. package/src/table/index.js +0 -1
  69. package/src/table/table.js +3 -58
  70. package/src/themes/blocks/unicode-half.js +0 -1
  71. package/src/themes/blocks/unicode-thin.js +0 -1
  72. package/src/themes/lines/ascii-compact.js +5 -9
  73. package/src/themes/lines/ascii-dots.js +2 -7
  74. package/src/themes/lines/ascii-girder.js +4 -7
  75. package/src/themes/lines/ascii-github.js +5 -9
  76. package/src/themes/lines/ascii-reddit.js +5 -9
  77. package/src/themes/lines/ascii-rounded.js +5 -9
  78. package/src/themes/lines/ascii.js +5 -9
  79. package/src/themes/lines/unicode-bold.js +8 -14
  80. package/src/themes/lines/unicode-rounded.js +8 -14
  81. package/src/themes/lines/unicode.js +8 -14
  82. package/src/themes/utils.d.ts +6 -0
  83. package/src/themes/utils.js +6 -7
  84. package/src/turtle/draw-line-art.js +26 -18
  85. package/src/turtle/draw-unicode.js +0 -8
  86. package/src/turtle/index.d.ts +1 -1
  87. package/src/turtle/index.js +0 -8
  88. package/src/turtle/turtle.js +0 -120
@@ -11,13 +11,6 @@ import defaultTheme from '../themes/default.js';
11
11
 
12
12
  const defaultSymbol = hBlocks8th[7];
13
13
 
14
- /** Draws a single stacked column using plain symbols.
15
- * @param {object[]} data - Normalized data series.
16
- * @param {number} width - Total height.
17
- * @param {number} maxValue - Maximum value for scaling.
18
- * @param {object} [options] - Options including `reverse`, `rectSize`, `initState`, `theme`.
19
- * @returns {string[]} The drawn column lines.
20
- */
21
14
  export const drawColumn = (data, width, maxValue, options = {}) => {
22
15
  const {reverse, rectSize = 1, initState = {}, theme = defaultTheme} = options,
23
16
  {symbol = ' ', state = null, colorState} = theme?.empty || {},
@@ -41,12 +34,6 @@ export const drawColumn = (data, width, maxValue, options = {}) => {
41
34
  return result.map(line => optimize(line));
42
35
  };
43
36
 
44
- /** Draws a complete plain stacked column chart.
45
- * @param {any[]} values - Chart data.
46
- * @param {number} width - Available height.
47
- * @param {object} [options] - Options.
48
- * @returns {string[]} Array of strings representing the chart.
49
- */
50
37
  export const drawChart = drawStackedChart(drawColumn);
51
38
 
52
39
  export default drawChart;
@@ -3,7 +3,6 @@ import style from '../../style.js';
3
3
 
4
4
  const seriesColors = 'cyan,magenta,blue,yellow,green,red'.split(',');
5
5
 
6
- /** The default chart theme with standard colors. */
7
6
  export const chartTheme = [
8
7
  ...seriesColors.map(name => ({colorState: style['bright' + capitalize(name)].getState()})),
9
8
  ...seriesColors.map(name => ({colorState: style[name].getState()}))
@@ -1,6 +1,5 @@
1
1
  import rainbow from './rainbow.js';
2
2
 
3
- /** Rainbow chart theme with reversed spectrum colors. */
4
3
  export const chartTheme = rainbow.slice().reverse();
5
4
 
6
5
  export default chartTheme;
@@ -3,7 +3,6 @@ import style from '../../style.js';
3
3
  // red, orange, yellow, green, blue, indigo, violet
4
4
  const colors = [0xff0000, 0xffa500, 0xffff00, 0x008000, 0x0000ff, 0x4b0082, 0xee82ee];
5
5
 
6
- /** Rainbow chart theme with spectrum colors. */
7
6
  export const chartTheme = colors.map(color => ({colorState: style.hex(color).getState()}));
8
7
 
9
8
  export default chartTheme;
@@ -1,10 +1,6 @@
1
1
  import {Commands} from '../ansi/sgr.js';
2
2
  import defaultTheme from './themes/default.js';
3
3
 
4
- /** Converts a foreground color state to a background color state.
5
- * @param {object} state - SGR state with a `foreground` property.
6
- * @returns {object} State with a `background` property.
7
- */
8
4
  export const makeBgFromFg = state => ({
9
5
  background: !state.foreground
10
6
  ? null
@@ -13,10 +9,6 @@ export const makeBgFromFg = state => ({
13
9
  : Number(state.foreground) + 10
14
10
  });
15
11
 
16
- /** Converts a background color state to a foreground color state.
17
- * @param {object} state - SGR state with a `background` property.
18
- * @returns {object} State with a `foreground` property.
19
- */
20
12
  export const makeFgFromBg = state => ({
21
13
  foreground: !state.background
22
14
  ? null
@@ -25,17 +17,8 @@ export const makeFgFromBg = state => ({
25
17
  : Number(state.background) - 10
26
18
  });
27
19
 
28
- /** Sums the values in a data series.
29
- * @param {{value?: number}[]} series
30
- * @returns {number}
31
- */
32
20
  export const sumValues = series => series.reduce((acc, datum) => acc + (datum?.value || 0), 0);
33
21
 
34
- /** Normalizes chart data, merging with default and custom themes.
35
- * @param {(number|object|Array)[]} data - Raw chart data.
36
- * @param {object[]} theme - Theme array for series styling.
37
- * @returns {object[][]} Normalized data.
38
- */
39
22
  export const normalizeData = (data, theme) =>
40
23
  data.map(series => {
41
24
  if (!Array.isArray(series)) series = [series];
@@ -48,12 +31,6 @@ export const normalizeData = (data, theme) =>
48
31
  });
49
32
  });
50
33
 
51
- /** Allocates pixel/character sizes to data values proportionally.
52
- * @param {{value?: number}[]} data - Data series.
53
- * @param {number} maxValue - Maximum value (-1 for auto).
54
- * @param {number} size - Total available size.
55
- * @returns {number[]} Allocated sizes per datum plus one extra for remainder.
56
- */
57
34
  export const allocateSizes = (data, maxValue, size) => {
58
35
  const values = data.map((datum, index) => ({value: datum?.value || 0, index})),
59
36
  seriesValue = values.reduce((acc, datum) => acc + datum.value, 0);
@@ -61,6 +38,8 @@ export const allocateSizes = (data, maxValue, size) => {
61
38
  if (maxValue < 0) {
62
39
  maxValue = seriesValue;
63
40
  if (!maxValue) maxValue = 1;
41
+ } else if (!maxValue) {
42
+ maxValue = 1;
64
43
  }
65
44
  if (seriesValue < maxValue) {
66
45
  values.push({value: maxValue - seriesValue, index: -1}); // add an empty bin
@@ -88,11 +67,6 @@ export const allocateSizes = (data, maxValue, size) => {
88
67
  return sizes;
89
68
  };
90
69
 
91
- /** Calculates the integer size needed for a fractional value.
92
- * @param {number} value - The fractional value.
93
- * @param {boolean} [drawEmptyBorder] - Whether to draw a border for empty fractional parts.
94
- * @returns {number} The integer size.
95
- */
96
70
  export const getFracSize = (value, drawEmptyBorder) => {
97
71
  const intValue = Math.floor(value),
98
72
  hasFrac = value - intValue > 0,
@@ -1,13 +1,6 @@
1
1
  import {fullBlock, hBlocks8th, vBlocks8th} from './symbols.js';
2
2
  import Box from './box.js';
3
3
 
4
- /** Draws a block with a fractional width using 1/8th Unicode block characters.
5
- * The integer part is filled with full blocks; the fractional part appears on the right.
6
- * @param {number} realWidth - The real width (float). Fractional part is interpreted in 1/8th steps.
7
- * @param {number} height - The integer height of the block.
8
- * @param {boolean} [drawEmptyBorder=false] - If true, add an empty border column when the fractional part is close to 0 but not exactly 0.
9
- * @returns {import('./box.js').Box} A Box containing the drawn block.
10
- */
11
4
  export const drawRealWidthBlock = (realWidth, height, drawEmptyBorder) => {
12
5
  realWidth = Math.max(0, realWidth);
13
6
  height = Math.max(0, Math.floor(height));
@@ -22,13 +15,6 @@ export const drawRealWidthBlock = (realWidth, height, drawEmptyBorder) => {
22
15
  return new Box(new Array(height).fill(line), true);
23
16
  };
24
17
 
25
- /** Draws a block with a fractional height using 1/8th Unicode block characters.
26
- * The integer part is filled with full blocks; the fractional part appears on the top.
27
- * @param {number} width - The integer width of the block.
28
- * @param {number} realHeight - The real height (float). Fractional part is interpreted in 1/8th steps.
29
- * @param {boolean} [drawEmptyBorder=false] - If true, add an empty border row when the fractional part is close to 0 but not exactly 0.
30
- * @returns {import('./box.js').Box} A Box containing the drawn block.
31
- */
32
18
  export const drawRealHeightBlock = (width, realHeight, drawEmptyBorder) => {
33
19
  width = Math.max(0, Math.floor(width));
34
20
  realHeight = Math.max(0, realHeight);
package/src/draw-block.js CHANGED
@@ -11,23 +11,6 @@ const T = 1,
11
11
  LB = getIndex(L, B),
12
12
  RB = getIndex(R, B);
13
13
 
14
- /** Draws a filled rectangular block using a block theme.
15
- * The interior is filled with a symbol from the theme's `f` property, or space by default.
16
- * @param {number} width - Interior width in columns.
17
- * @param {number} height - Interior height in rows.
18
- * @param {object} blockTheme - Block theme object defining border characters.
19
- * @param {object} [options] - Drawing options.
20
- * @param {number} [options.top] - Sub-theme for the top border (defaults to `hTheme`, then `theme`, then `1`).
21
- * @param {number} [options.bottom] - Sub-theme for the bottom border (defaults to `hTheme`, then `theme`, then `1`).
22
- * @param {number} [options.left] - Sub-theme for the left border (defaults to `vTheme`, then `theme`, then `1`).
23
- * @param {number} [options.right] - Sub-theme for the right border (defaults to `vTheme`, then `theme`, then `1`).
24
- * @param {number} [options.vTheme] - Sub-theme for vertical borders (left and right). Defaults to `theme`, then `1`.
25
- * @param {number} [options.hTheme] - Sub-theme for horizontal borders (top and bottom). Defaults to `theme`, then `1`.
26
- * @param {number} [options.theme] - Sub-theme for all borders (default: `1`).
27
- * @param {string} [options.symbol] - Fill character for the interior. Defaults to the theme's `f` property or space.
28
- * @returns {import('./box.js').Box} A Box containing the drawn block.
29
- * @see {@link https://github.com/uhop/console-toolkit/wiki/Module:-draw-block}
30
- */
31
14
  export const drawBlock = (
32
15
  width,
33
16
  height,
@@ -64,13 +47,6 @@ export const drawBlock = (
64
47
  );
65
48
  };
66
49
 
67
- /** Draws a rectangular frame using a block theme. Same as `drawBlock()` but defaults the interior fill to space if `symbol` is not specified.
68
- * @param {number} width - Interior width in columns.
69
- * @param {number} height - Interior height in rows.
70
- * @param {object} blockTheme - Block theme object defining border characters.
71
- * @param {object} [options] - Drawing options (same as `drawBlock()`).
72
- * @returns {import('./box.js').Box} A Box containing the drawn frame.
73
- */
74
50
  export const drawFrame = (width, height, blockTheme, options) => {
75
51
  if (!options?.symbol) options = {...options, symbol: ' '};
76
52
  return drawBlock(width, height, blockTheme, options);
package/src/meta.js CHANGED
@@ -1,90 +1,31 @@
1
- /** Capitalizes the first letter of a string and lowercases the rest.
2
- * @param {string} name - The string to capitalize.
3
- * @returns {string} The capitalized string.
4
- */
5
1
  export const capitalize = name => (name ? name[0].toUpperCase() + name.substring(1).toLowerCase() : name);
6
2
 
7
- /** Converts an array of name parts to camelCase.
8
- * @param {string[]} names - The name parts.
9
- * @returns {string} The camelCase string.
10
- */
11
3
  export const toCamelCase = names =>
12
4
  names.map((name, index) => (index ? capitalize(name) : name.toLowerCase())).join('');
13
- /** Splits a camelCase string into an array of name parts.
14
- * @param {string} name - The camelCase string.
15
- * @returns {string[]} The name parts.
16
- */
17
5
  export const fromCamelCase = name => name.split(/(?=[A-Z])/g);
18
6
 
19
- /** Converts an array of name parts to PascalCase.
20
- * @param {string[]} names - The name parts.
21
- * @returns {string} The PascalCase string.
22
- */
23
7
  export const toPascalCase = names => names.map(name => capitalize(name)).join('');
24
- /** Splits a PascalCase string into an array of name parts.
25
- * @param {string} name - The PascalCase string.
26
- * @returns {string[]} The name parts.
27
- */
28
8
  export const fromPascalCase = name => name.split(/(?=[A-Z])/g);
29
9
 
30
- /** Converts an array of name parts to ALL_CAPS_SNAKE_CASE.
31
- * @param {string[]} names - The name parts.
32
- * @returns {string} The ALL_CAPS_SNAKE_CASE string.
33
- */
34
10
  export const toAllCapsSnakeCase = names => names.map(name => name.toUpperCase()).join('_');
35
- /** Converts an array of name parts to snake_case.
36
- * @param {string[]} names - The name parts.
37
- * @returns {string} The snake_case string.
38
- */
39
11
  export const toSnakeCase = names => names.map(name => name.toLowerCase()).join('_');
40
- /** Splits a snake_case string into an array of name parts.
41
- * @param {string} name - The snake_case string.
42
- * @returns {string[]} The name parts.
43
- */
44
12
  export const fromSnakeCase = name => name.split('_');
45
13
 
46
- /** Converts an array of name parts to kebab-case.
47
- * @param {string[]} names - The name parts.
48
- * @returns {string} The kebab-case string.
49
- */
50
14
  export const toKebabCase = names => names.map(name => name.toLowerCase()).join('-');
51
- /** Splits a kebab-case string into an array of name parts.
52
- * @param {string} name - The kebab-case string.
53
- * @returns {string[]} The name parts.
54
- */
55
15
  export const fromKebabCase = name => name.split('-');
56
16
 
57
- /** Adds a getter property to a class prototype or object.
58
- * @param {Function|object} Class - The class or object to add the getter to.
59
- * @param {string} name - The property name.
60
- * @param {Function} getter - The getter function.
61
- * @param {boolean} [force] - If true, overwrite existing properties.
62
- * @returns {object} The modified prototype or object.
63
- */
64
17
  export const addGetter = (Class, name, getter, force) => {
65
18
  const object = Class.prototype || Class;
66
19
  if (!force && object.hasOwnProperty(name)) return object;
67
20
  return Object.defineProperty(object, name, {configurable: true, enumerable: true, get: getter});
68
21
  };
69
22
 
70
- /** Adds multiple getter properties to a class prototype or object.
71
- * @param {Function|object} Class - The class or object to add getters to.
72
- * @param {Record<string, Function>} getters - An object mapping property names to getter functions.
73
- * @param {boolean} [force] - If true, overwrite existing properties.
74
- */
75
23
  export const addGetters = (Class, getters, force) => {
76
24
  for (const [name, value] of Object.entries(getters)) {
77
25
  addGetter(Class, name, value, force);
78
26
  }
79
27
  };
80
28
 
81
- /** Adds an alias property that mirrors an existing property descriptor.
82
- * @param {Function|object} Class - The class or object to add the alias to.
83
- * @param {string} name - The new alias name.
84
- * @param {string} oldName - The existing property name to alias.
85
- * @param {boolean} [force] - If true, overwrite existing properties.
86
- * @returns {object} The modified prototype or object.
87
- */
88
29
  export const addAlias = (Class, name, oldName, force) => {
89
30
  const object = Class.prototype || Class;
90
31
  if (!force && object.hasOwnProperty(name)) return object;
@@ -93,11 +34,6 @@ export const addAlias = (Class, name, oldName, force) => {
93
34
  return Object.defineProperty(object, name, descriptor);
94
35
  };
95
36
 
96
- /** Adds multiple alias properties to a class prototype or object.
97
- * @param {Function|object} Class - The class or object to add aliases to.
98
- * @param {Record<string, string>} aliases - An object mapping new names to existing property names.
99
- * @param {boolean} [force] - If true, overwrite existing properties.
100
- */
101
37
  export const addAliases = (Class, aliases, force) => {
102
38
  for (const [name, oldName] of Object.entries(aliases)) {
103
39
  addAlias(Class, name, oldName, force);
@@ -19,18 +19,23 @@ export interface OutOptions {
19
19
  colorDepth?: number;
20
20
  }
21
21
 
22
- /** Logs a text container to the console via `console.log()`. Strips ANSI codes if colorDepth < 4.
22
+ /** Logs a text container via `console.log()`. Defaults to passing ANSI through (colorDepth = 24).
23
+ * Use this when you know your terminal supports the colors you're emitting.
24
+ * Strips ANSI when colorDepth < 4.
23
25
  * @param s - Input: Box, string, string array, or object with `toStrings()`.
24
26
  * @param options - Log options.
25
27
  */
26
28
  export function log(s: StringsInput, options?: LogOptions): void;
27
- /** Writes a text container to a stream (default: stdout).
29
+ /** Writes a text container to any writable stream (default: stdout).
30
+ * Auto-detects colorDepth from `stream.isTTY` and `stream.getColorDepth()` when omitted.
31
+ * Strips ANSI when colorDepth < 4.
28
32
  * @param s - Input: Box, string, string array, or object with `toStrings()`.
29
33
  * @param options - Output options.
30
34
  */
31
35
  export function out(s: StringsInput, options?: OutOptions): void;
32
36
 
33
- /** Wraps a writable stream for outputting styled text. */
37
+ /** Wraps a writable stream for outputting styled text. Caches `colorDepth` once at construction
38
+ * to avoid per-call detection — useful for tight write loops to the same stream. */
34
39
  export class Out {
35
40
  /** The underlying writable stream. */
36
41
  stream: Writable;
@@ -2,66 +2,35 @@ import process from 'node:process';
2
2
  import {matchCsiNoGroups} from '../strings.js';
3
3
  import Box from '../box.js';
4
4
 
5
- /** Logs a text container to the console via `console.log()`. Strips ANSI codes if colorDepth < 4.
6
- * @param {import('../strings.js').StringsInput} s - Input convertible to a Box.
7
- * @param {object} [options] - Options.
8
- * @param {string} [options.endOfLineCommand='\x1B[m'] - ANSI command appended to each line.
9
- * @param {number} [options.colorDepth=24] - Color depth (1 = no color, 4/8/24 = color).
10
- */
11
- export const log = (s, {endOfLineCommand = '\x1B[m', colorDepth = 24} = {}) => {
5
+ const writeRows = (s, endOfLineCommand, colorDepth, write) => {
12
6
  s = Box.make(s);
13
7
  if (colorDepth < 4) {
14
8
  matchCsiNoGroups.lastIndex = 0;
15
- s.box.forEach(row => console.log((row + endOfLineCommand).replace(matchCsiNoGroups, '')));
9
+ s.box.forEach(row => write((row + endOfLineCommand).replace(matchCsiNoGroups, '')));
16
10
  return;
17
11
  }
18
- s.box.forEach(row => console.log(row + endOfLineCommand));
12
+ s.box.forEach(row => write(row + endOfLineCommand));
19
13
  };
20
14
 
21
- /** Writes a text container to a stream. Strips ANSI codes if colorDepth < 4.
22
- * @param {import('../strings.js').StringsInput} s - Input convertible to a Box.
23
- * @param {object} [options] - Options.
24
- * @param {import('node:stream').Writable} [options.stream=process.stdout] - The output stream.
25
- * @param {string} [options.endOfLineCommand='\x1B[m'] - ANSI command appended to each line.
26
- * @param {number} [options.colorDepth] - Color depth. Auto-detected from stream if not specified.
27
- */
15
+ export const log = (s, {endOfLineCommand = '\x1B[m', colorDepth = 24} = {}) =>
16
+ writeRows(s, endOfLineCommand, colorDepth, row => console.log(row));
17
+
28
18
  export const out = (s, {stream = process.stdout, endOfLineCommand = '\x1B[m', colorDepth} = {}) => {
29
- s = Box.make(s);
30
19
  if (typeof colorDepth != 'number' || isNaN(colorDepth)) colorDepth = stream.isTTY ? stream.getColorDepth() : 1;
31
- if (colorDepth < 4) {
32
- matchCsiNoGroups.lastIndex = 0;
33
- s.box.forEach(row => stream.write((row + endOfLineCommand).replace(matchCsiNoGroups, '') + '\n'));
34
- return;
35
- }
36
- s.box.forEach(row => stream.write(row + endOfLineCommand + '\n'));
20
+ writeRows(s, endOfLineCommand, colorDepth, row => stream.write(row + '\n'));
37
21
  };
38
22
 
39
- /** Convenience wrapper around a writable stream for formatted output.
40
- * Auto-detects color depth from the stream.
41
- */
42
23
  export class Out {
43
- /**
44
- * @param {import('node:stream').Writable} stream - The writable stream to wrap.
45
- */
46
24
  constructor(stream) {
47
25
  this.stream = stream;
48
26
  this.colorDepth = stream.isTTY ? stream.getColorDepth() : 1;
49
27
  }
50
- /** Writes a text container to the stream.
51
- * @param {import('../strings.js').StringsInput} s - Input convertible to a Box.
52
- * @param {object} [options] - Options.
53
- * @param {string} [options.endOfLineCommand='\x1B[m'] - ANSI command appended to each line.
54
- * @param {number} [options.colorDepth] - Color depth override.
55
- */
56
28
  out(s, {endOfLineCommand = '\x1B[m', colorDepth} = {}) {
57
29
  if (typeof colorDepth != 'number' || isNaN(colorDepth)) colorDepth = this.colorDepth;
58
30
  return out(s, {stream: this.stream, endOfLineCommand, colorDepth});
59
31
  }
60
32
  }
61
33
 
62
- /** Logs a string with control characters visualized as hex escape sequences.
63
- * @param {string} string - The string to debug.
64
- */
65
34
  export const debug = string =>
66
35
  console.log(
67
36
  string.replace(/[\x00-\x1F]/g, m => '\\x' + m[0].charCodeAt(0).toString(16).padStart(2, '0').toUpperCase())
@@ -19,14 +19,19 @@ export interface UpdaterOptions {
19
19
  noLastNewLine?: boolean;
20
20
  }
21
21
 
22
- /** An object that can provide frames for the Updater. */
22
+ /** An object that can provide frames for the Updater. Prefer implementing `nextFrame()`
23
+ * (advance + return) for spinners and progress bars; `getFrame()` is the read-only fallback. */
23
24
  export interface UpdaterTarget {
24
25
  /** Current state string. */
25
26
  state?: string;
26
- /** Returns the current frame content.
27
+ /** Returns the next frame content (advance + return). Preferred entry point for spinners.
27
28
  * @returns Frame content (Box, string, or string array).
28
29
  */
29
- getFrame(...args: unknown[]): StringsInput;
30
+ nextFrame?(...args: unknown[]): StringsInput;
31
+ /** Returns the current frame content. Used as fallback when `nextFrame()` is not implemented.
32
+ * @returns Frame content (Box, string, or string array).
33
+ */
34
+ getFrame?(...args: unknown[]): StringsInput;
30
35
  }
31
36
 
32
37
  /** Manages auto-refreshing console output for spinners, progress bars, etc. */
@@ -4,22 +4,7 @@ import {cursorUp, setCommands} from '../ansi/csi.js';
4
4
 
5
5
  const RESET = setCommands([]);
6
6
 
7
- /** Manages continuously updating console output (spinners, progress bars, etc.).
8
- * Handles refreshing frames, prologue/epilogue sequences, and interacts with a Writer instance.
9
- */
10
7
  export class Updater {
11
- /**
12
- * @param {((state: string, ...args: unknown[]) => import('../strings.js').StringsInput)|{state?: string, getFrame: (...args: unknown[]) => import('../strings.js').StringsInput}} updater - A function `(state, ...args) => frame` or an object with `getFrame()`.
13
- * @param {object} [options] - Options.
14
- * @param {string} [options.prologue] - String written before the first frame.
15
- * @param {string} [options.epilogue] - String written after the last frame.
16
- * @param {string} [options.beforeFrame] - String written before each frame.
17
- * @param {string} [options.afterFrame] - String written after each frame.
18
- * @param {string} [options.beforeLine] - String prepended to each line.
19
- * @param {string} [options.afterLine] - String appended to each line.
20
- * @param {boolean} [options.noLastNewLine] - If true, omit the trailing newline of each frame.
21
- * @param {import('./writer.js').default} [writer=new Writer()] - The Writer instance to use.
22
- */
23
8
  constructor(
24
9
  updater,
25
10
  {prologue, epilogue, beforeFrame, afterFrame, beforeLine, afterLine, noLastNewLine} = {},
@@ -40,24 +25,16 @@ export class Updater {
40
25
  this.intervalHandle = null;
41
26
  }
42
27
 
43
- /** Whether the updater is currently auto-refreshing. */
44
28
  get isRefreshing() {
45
29
  return this.intervalHandle !== null;
46
30
  }
47
31
 
48
- /** Starts auto-refreshing at the given interval.
49
- * @param {number} [ms=100] - Refresh interval in milliseconds.
50
- * @returns {this}
51
- */
52
32
  startRefreshing(ms = 100) {
53
33
  if (this.intervalHandle || this.isDone || !this.writer.isTTY) return this;
54
34
  this.intervalHandle = setInterval(this.update.bind(this), ms);
55
35
  return this;
56
36
  }
57
37
 
58
- /** Stops auto-refreshing.
59
- * @returns {this}
60
- */
61
38
  stopRefreshing() {
62
39
  if (!this.intervalHandle) return this;
63
40
  clearInterval(this.intervalHandle);
@@ -65,9 +42,6 @@ export class Updater {
65
42
  return this;
66
43
  }
67
44
 
68
- /** Resets the updater state, stopping any refresh and clearing the done flag.
69
- * @returns {this}
70
- */
71
45
  reset() {
72
46
  this.stopRefreshing();
73
47
  this.isDone = false;
@@ -75,25 +49,16 @@ export class Updater {
75
49
  return this;
76
50
  }
77
51
 
78
- /** Gets a frame from the updater function or object.
79
- * @param {string} state - The current state ('active', 'paused', 'finished', etc.).
80
- * @param {...unknown} args - Additional arguments.
81
- * @returns {import('../strings.js').StringsInput} The frame content.
82
- */
83
52
  getFrame(state, ...args) {
84
53
  if (typeof this.updater == 'function') return this.updater(state, ...args);
85
- if (typeof this.updater?.getFrame == 'function') {
54
+ if (this.updater) {
86
55
  this.updater.state = state;
87
- return this.updater.getFrame(...args);
56
+ if (typeof this.updater.nextFrame == 'function') return this.updater.nextFrame(...args);
57
+ if (typeof this.updater.getFrame == 'function') return this.updater.getFrame(...args);
88
58
  }
89
- throw new TypeError('Updater must be a function or implement getFrame()');
59
+ throw new TypeError('Updater must be a function or implement nextFrame()/getFrame()');
90
60
  }
91
61
 
92
- /** Writes a single frame to the output, handling cursor repositioning.
93
- * @param {string} state - The current state.
94
- * @param {...unknown} args - Additional arguments passed to `getFrame()`.
95
- * @returns {Promise<void>}
96
- */
97
62
  async writeFrame(state, ...args) {
98
63
  if (this.first) {
99
64
  this.prologue && (await this.writer.writeString(this.prologue));
@@ -116,9 +81,6 @@ export class Updater {
116
81
  this.afterFrame && (await this.writer.writeString(this.afterFrame));
117
82
  }
118
83
 
119
- /** Marks the updater as done, stops refreshing, and writes the epilogue.
120
- * @returns {Promise<void>}
121
- */
122
84
  async done() {
123
85
  if (this.isDone) return;
124
86
  this.isDone = true;
@@ -126,20 +88,11 @@ export class Updater {
126
88
  this.epilogue && (await this.writer.writeString(this.epilogue));
127
89
  }
128
90
 
129
- /** Updates the display with a new frame.
130
- * @param {string} [state='active'] - The current state.
131
- * @param {...unknown} args - Additional arguments passed to `getFrame()`.
132
- * @returns {Promise<void>}
133
- */
134
91
  async update(state = 'active', ...args) {
135
92
  if (this.isDone || !this.writer.isTTY) return;
136
93
  await this.writeFrame(state, ...args);
137
94
  }
138
95
 
139
- /** Writes the final frame with state 'finished' and calls `done()`.
140
- * @param {...unknown} args - Additional arguments passed to `getFrame()`.
141
- * @returns {Promise<void>}
142
- */
143
96
  async final(...args) {
144
97
  if (this.isDone) return;
145
98
  await this.writeFrame('finished', ...args);
@@ -6,59 +6,35 @@ import {getLength, matchCsiNoGroups, matchCsiNoSgrNoGroups, toStrings} from '../
6
6
  const write = async (stream, chunk, encoding = 'utf8') =>
7
7
  new Promise((resolve, reject) => stream.write(chunk, encoding, error => (error ? reject(error) : resolve())));
8
8
 
9
- /** Abstracts writing to a stream (defaulting to `process.stdout`).
10
- * Handles TTY capabilities, color depth, cursor manipulation, and ANSI stripping for non-TTY streams.
11
- */
12
9
  export class Writer {
13
- /**
14
- * @param {import('node:stream').Writable} [stream=process.stdout] - The output stream.
15
- * @param {number} [forceColorDepth] - Force a specific color depth instead of auto-detecting.
16
- */
17
10
  constructor(stream = process.stdout, forceColorDepth) {
18
11
  this.stream = stream;
19
12
  this.forceColorDepth = forceColorDepth;
20
13
  }
21
14
 
22
- /** Whether the stream is a TTY. */
23
15
  get isTTY() {
24
16
  return this.stream.isTTY;
25
17
  }
26
- /** The number of columns in the terminal. */
27
18
  get columns() {
28
19
  return this.stream.columns;
29
20
  }
30
- /** The number of rows in the terminal. */
31
21
  get rows() {
32
22
  return this.stream.rows;
33
23
  }
34
- /** The terminal size as `{columns, rows}`. */
35
24
  get size() {
36
25
  const [columns, rows] = this.stream.getWindowSize?.() || [];
37
26
  return {columns, rows};
38
27
  }
39
- /** Returns the color depth of the stream.
40
- * @param {object} [env] - Environment variables to check (default: `process.env`).
41
- * @returns {number} The color depth (1, 4, 8, or 24).
42
- */
43
28
  getColorDepth(...args) {
44
29
  return this.forceColorDepth || this.stream.getColorDepth?.(...args);
45
30
  }
46
31
 
47
- /** Checks if the stream supports the given number of colors.
48
- * @param {number} [count] - Number of colors to check for.
49
- * @param {object} [env] - Environment variables to check.
50
- * @returns {boolean} True if supported.
51
- */
52
32
  hasColors(...args) {
53
33
  if (!this.forceColorDepth) return this.stream.hasColors?.(...args);
54
34
  const count = typeof args[0] == 'number' ? args[0] : 2;
55
35
  return count <= Math.pow(2, this.forceColorDepth);
56
36
  }
57
37
 
58
- /** Clears the current line.
59
- * @param {number} dir - Direction: -1 = left, 0 = entire line, 1 = right.
60
- * @returns {Promise<boolean>} True if the operation was supported.
61
- */
62
38
  clearLine(dir) {
63
39
  return new Promise((resolve, reject) => {
64
40
  if (typeof this.stream.clearLine == 'function') {
@@ -68,9 +44,6 @@ export class Writer {
68
44
  }
69
45
  });
70
46
  }
71
- /** Clears the screen from the cursor down.
72
- * @returns {Promise<boolean>} True if the operation was supported.
73
- */
74
47
  clearScreenDown() {
75
48
  return new Promise((resolve, reject) => {
76
49
  if (typeof this.stream.clearScreenDown == 'function') {
@@ -81,11 +54,6 @@ export class Writer {
81
54
  });
82
55
  }
83
56
 
84
- /** Moves the cursor to an absolute position.
85
- * @param {number} x - Column.
86
- * @param {number} [y] - Row.
87
- * @returns {Promise<boolean>} True if the operation was supported.
88
- */
89
57
  cursorTo(x, y) {
90
58
  return new Promise((resolve, reject) => {
91
59
  if (typeof this.stream.cursorTo == 'function') {
@@ -95,11 +63,6 @@ export class Writer {
95
63
  }
96
64
  });
97
65
  }
98
- /** Moves the cursor relative to its current position.
99
- * @param {number} dx - Columns to move.
100
- * @param {number} dy - Rows to move.
101
- * @returns {Promise<boolean>} True if the operation was supported.
102
- */
103
66
  moveCursor(dx, dy) {
104
67
  return new Promise((resolve, reject) => {
105
68
  if (typeof this.stream.moveCursor == 'function') {
@@ -110,10 +73,6 @@ export class Writer {
110
73
  });
111
74
  }
112
75
 
113
- /** Writes a raw string to the stream, stripping ANSI codes for non-TTY streams.
114
- * @param {string} s - The string to write.
115
- * @returns {Promise<void>}
116
- */
117
76
  async writeString(s) {
118
77
  s = String(s);
119
78
 
@@ -134,15 +93,6 @@ export class Writer {
134
93
  await write(this.stream, s.replace(matchCsiNoGroups, ''));
135
94
  }
136
95
 
137
- /** Writes a text container to the stream.
138
- * @param {import('../strings.js').StringsInput} s - Input convertible to strings.
139
- * @param {object} [options] - Options.
140
- * @param {boolean|'save'} [options.sameColumn] - If true or 'save', keep cursor in the same column between lines.
141
- * @param {boolean} [options.noLastNewLine] - If true, omit the trailing newline.
142
- * @param {string} [options.beforeLine=''] - String prepended to each line.
143
- * @param {string} [options.afterLine=''] - String appended to each line.
144
- * @returns {Promise<void>}
145
- */
146
96
  async write(s, {sameColumn, noLastNewLine, beforeLine = '', afterLine = ''} = {}) {
147
97
  s = toStrings(s);
148
98
 
@@ -175,9 +125,7 @@ export class Writer {
175
125
  return;
176
126
  }
177
127
 
178
- let lines = Array.from(s)
179
- .map(line => beforeLine + line + afterLine)
180
- .join('\n');
128
+ let lines = s.map(line => beforeLine + line + afterLine).join('\n');
181
129
  if (!noLastNewLine) lines += '\n';
182
130
  await write(this.stream, lines);
183
131
  }