cli-kiss 0.2.3 → 0.2.5

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/src/lib/Typo.ts CHANGED
@@ -25,6 +25,10 @@ export type TypoColor =
25
25
  * All fields are optional; ignored entirely in `"none"` mode.
26
26
  */
27
27
  export type TypoStyle = {
28
+ /**
29
+ * Letter case.
30
+ */
31
+ case?: "upper" | "lower";
28
32
  /**
29
33
  * Foreground (text) color.
30
34
  */
@@ -34,43 +38,43 @@ export type TypoStyle = {
34
38
  */
35
39
  bgColor?: TypoColor;
36
40
  /**
37
- * Render with reduced intensity.
41
+ * Reduced intensity.
38
42
  */
39
43
  dim?: boolean;
40
44
  /**
41
- * Render in bold.
45
+ * Bold.
42
46
  */
43
47
  bold?: boolean;
44
48
  /**
45
- * Render in italic.
49
+ * Italic.
46
50
  */
47
51
  italic?: boolean;
48
52
  /**
49
- * Render with an underline.
53
+ * Underline.
50
54
  */
51
55
  underline?: boolean;
52
56
  /**
53
- * Render with a strikethrough.
57
+ * Strikethrough.
54
58
  */
55
59
  strikethrough?: boolean;
56
60
  };
57
61
 
58
62
  /**
59
- * Pre-defined style for section titles (e.g. `"Positionals:"`). Bold dark-green.
63
+ * Section title style. Bold dark-green.
60
64
  */
61
65
  export const typoStyleTitle: TypoStyle = {
62
66
  fgColor: "darkGreen",
63
67
  bold: true,
64
68
  };
65
69
  /**
66
- * Pre-defined style for logic/type identifiers in error messages. Bold dark-magenta.
70
+ * Logic/type identifier style. Bold dark-magenta.
67
71
  */
68
72
  export const typoStyleLogic: TypoStyle = {
69
73
  fgColor: "darkMagenta",
70
74
  bold: true,
71
75
  };
72
76
  /**
73
- * Pre-defined style for quoted user-supplied values in error messages. Bold dark-yellow.
77
+ * Quoted user-input style. Bold dark-yellow.
74
78
  */
75
79
  export const typoStyleQuote: TypoStyle = {
76
80
  fgColor: "darkYellow",
@@ -78,7 +82,7 @@ export const typoStyleQuote: TypoStyle = {
78
82
  };
79
83
 
80
84
  /**
81
- * Pre-defined style for failure/error labels (e.g. `"Error:"`). Bold dark-red.
85
+ * Error label style. Bold dark-red.
82
86
  */
83
87
  export const typoStyleFailure: TypoStyle = {
84
88
  fgColor: "darkRed",
@@ -86,14 +90,14 @@ export const typoStyleFailure: TypoStyle = {
86
90
  };
87
91
 
88
92
  /**
89
- * Pre-defined style for CLI flag/option/command names. Bold dark-cyan.
93
+ * Option/command name style. Bold dark-cyan.
90
94
  */
91
95
  export const typoStyleConstants: TypoStyle = {
92
96
  fgColor: "darkCyan",
93
97
  bold: true,
94
98
  };
95
99
  /**
96
- * Pre-defined style for positional placeholders and user-input labels. Bold dark-blue.
100
+ * Positional/user-input label style. Bold dark-blue.
97
101
  */
98
102
  export const typoStyleUserInput: TypoStyle = {
99
103
  fgColor: "darkBlue",
@@ -101,13 +105,13 @@ export const typoStyleUserInput: TypoStyle = {
101
105
  };
102
106
 
103
107
  /**
104
- * Pre-defined style for strong regular text (e.g. command descriptions). Bold.
108
+ * Strong text style. Bold.
105
109
  */
106
110
  export const typoStyleRegularStrong: TypoStyle = {
107
111
  bold: true,
108
112
  };
109
113
  /**
110
- * Pre-defined style for subtle supplementary text (e.g. hints). Italic and dim.
114
+ * Subtle text style. Italic and dim.
111
115
  */
112
116
  export const typoStyleRegularWeaker: TypoStyle = {
113
117
  italic: true,
@@ -115,8 +119,7 @@ export const typoStyleRegularWeaker: TypoStyle = {
115
119
  };
116
120
 
117
121
  /**
118
- * An immutable styled string segment: a raw text value paired with a {@link TypoStyle}.
119
- * Compose multiple segments into a {@link TypoText}; rendering is deferred to {@link TypoString.computeStyledString}.
122
+ * Immutable styled string segment. Compose into a {@link TypoText}.
120
123
  */
121
124
  export class TypoString {
122
125
  #value: string;
@@ -129,12 +132,6 @@ export class TypoString {
129
132
  this.#value = value;
130
133
  this.#typoStyle = typoStyle;
131
134
  }
132
- /**
133
- * Returns the unstyled raw text content.
134
- */
135
- getRawString(): string {
136
- return this.#value;
137
- }
138
135
  /**
139
136
  * Returns the text styled by `typoSupport`.
140
137
  *
@@ -143,53 +140,46 @@ export class TypoString {
143
140
  computeStyledString(typoSupport: TypoSupport): string {
144
141
  return typoSupport.computeStyledString(this.#value, this.#typoStyle);
145
142
  }
143
+ /**
144
+ * Returns the raw text.
145
+ */
146
+ getRawString(): string {
147
+ return this.#value;
148
+ }
146
149
  }
147
150
 
148
151
  /**
149
- * A mutable sequence of {@link TypoString} segments forming a styled multi-part message.
150
- * Rendering is deferred to {@link TypoText.computeStyledString}.
152
+ * Mutable sequence of {@link TypoString} segments.
151
153
  */
152
154
  export class TypoText {
153
155
  #typoStrings: Array<TypoString>;
154
156
  /**
155
- * @param typoParts - Initial segments; `TypoText` is flattened, `string` is wrapped unstyled.
157
+ * @param segments - Initial text segments
156
158
  */
157
- constructor(...typoParts: Array<TypoText | TypoString | string>) {
159
+ constructor(
160
+ ...segments: Array<TypoText | Array<TypoString> | TypoString | string>
161
+ ) {
158
162
  this.#typoStrings = [];
159
- for (const typoPart of typoParts) {
160
- if (typoPart instanceof TypoText) {
161
- this.pushText(typoPart);
162
- } else if (typoPart instanceof TypoString) {
163
- this.pushString(typoPart);
164
- } else if (typeof typoPart === "string") {
165
- this.pushString(new TypoString(typoPart));
166
- }
167
- }
168
- }
169
- /**
170
- * Appends a {@link TypoString} segment.
171
- *
172
- * @param typoString - Segment to append.
173
- */
174
- pushString(typoString: TypoString | string) {
175
- if (typeof typoString === "string") {
176
- this.#typoStrings.push(new TypoString(typoString));
177
- } else {
178
- this.#typoStrings.push(typoString);
163
+ for (const typoPart of segments) {
164
+ this.push(typoPart);
179
165
  }
180
166
  }
181
167
  /**
182
- * Appends all segments from another {@link TypoText} (shallow copy).
168
+ * Appends new text segment(s).
183
169
  *
184
- * @param typoText - Source text.
170
+ * @param segment - Text segment(s) to append.
185
171
  */
186
- pushText(typoText: TypoText | string) {
187
- if (typeof typoText === "string") {
188
- this.pushString(typoText);
189
- } else {
190
- for (const typoString of typoText.#typoStrings) {
172
+ push(segment: TypoText | Array<TypoString> | TypoString | string) {
173
+ if (typeof segment === "string") {
174
+ this.#typoStrings.push(new TypoString(segment));
175
+ } else if (segment instanceof TypoText) {
176
+ this.#typoStrings.push(...segment.#typoStrings);
177
+ } else if (Array.isArray(segment)) {
178
+ for (const typoString of segment) {
191
179
  this.#typoStrings.push(typoString);
192
180
  }
181
+ } else {
182
+ this.#typoStrings.push(segment);
193
183
  }
194
184
  }
195
185
  /**
@@ -204,13 +194,13 @@ export class TypoText {
204
194
  .join("");
205
195
  }
206
196
  /**
207
- * Returns the concatenation of all segments' raw (unstyled) text.
197
+ * Returns the concatenated raw text.
208
198
  */
209
199
  computeRawString(): string {
210
200
  return this.#typoStrings.map((t) => t.getRawString()).join("");
211
201
  }
212
202
  /**
213
- * Returns the total character count of the raw (unstyled) text.
203
+ * Returns the total raw character count.
214
204
  */
215
205
  computeRawLength(): number {
216
206
  let length = 0;
@@ -222,9 +212,8 @@ export class TypoText {
222
212
  }
223
213
 
224
214
  /**
225
- * A column-aligned grid of {@link TypoText} cells.
215
+ * Column-aligned grid of {@link TypoText} cells.
226
216
  * Each column is padded to the widest cell (raw chars); the last column is not padded.
227
- * Used by {@link usageToStyledLines} to render `Positionals:`, `Subcommands:`, and `Options:`.
228
217
  */
229
218
  export class TypoGrid {
230
219
  #typoRows: Array<Array<TypoText>>;
@@ -232,7 +221,7 @@ export class TypoGrid {
232
221
  this.#typoRows = [];
233
222
  }
234
223
  /**
235
- * Appends a row. All rows should have the same cell count for alignment to be meaningful.
224
+ * Appends a row. All rows should have the same cell count.
236
225
  *
237
226
  * @param cells - Ordered {@link TypoText} cells.
238
227
  */
@@ -240,15 +229,14 @@ export class TypoGrid {
240
229
  this.#typoRows.push(cells);
241
230
  }
242
231
  /**
243
- * Renders the grid as a 2-D array of styled (and column-padded) strings.
244
- * Join each inner array with `""` to get a line.
232
+ * Renders as an array of styled, column-padded strings.
245
233
  *
246
234
  * @param typoSupport - Rendering mode.
247
- * @returns 2-D array of styled strings.
235
+ * @returns Array of styled strings.
248
236
  */
249
- computeStyledGrid(typoSupport: TypoSupport): Array<Array<string>> {
237
+ computeStyledLines(typoSupport: TypoSupport): Array<string> {
250
238
  const widths = new Array<number>();
251
- const printableGrid = new Array<Array<string>>();
239
+ const styledLines = new Array<string>();
252
240
  for (const typoGridRow of this.#typoRows) {
253
241
  for (
254
242
  let typoGridColumnIndex = 0;
@@ -266,31 +254,29 @@ export class TypoGrid {
266
254
  }
267
255
  }
268
256
  for (const typoGridRow of this.#typoRows) {
269
- const printableGridRow = new Array<string>();
257
+ const styledGridRow = new Array<string>();
270
258
  for (
271
259
  let typoGridColumnIndex = 0;
272
260
  typoGridColumnIndex < typoGridRow.length;
273
261
  typoGridColumnIndex++
274
262
  ) {
275
263
  const typoGridCell = typoGridRow[typoGridColumnIndex]!;
276
- const printableGridCell = typoGridCell.computeStyledString(typoSupport);
277
- printableGridRow.push(printableGridCell);
264
+ styledGridRow.push(typoGridCell.computeStyledString(typoSupport));
278
265
  if (typoGridColumnIndex < typoGridRow.length - 1) {
279
266
  const width = typoGridCell.computeRawLength();
280
267
  const padding = " ".repeat(widths[typoGridColumnIndex]! - width);
281
- printableGridRow.push(padding);
268
+ styledGridRow.push(padding);
282
269
  }
283
270
  }
284
- printableGrid.push(printableGridRow);
271
+ styledLines.push(styledGridRow.join(""));
285
272
  }
286
- return printableGrid;
273
+ return styledLines;
287
274
  }
288
275
  }
289
276
 
290
277
  /**
291
278
  * `Error` subclass with a {@link TypoText} styled message for rich terminal output.
292
- * Used throughout the library for parse failures (unknown option, type decode error, etc.).
293
- * If `source` is a `TypoError`, its styled text is chained after `": "`.
279
+ * Chains `TypoError` sources after `": "`.
294
280
  */
295
281
  export class TypoError extends Error {
296
282
  #typoText: TypoText;
@@ -300,20 +286,20 @@ export class TypoError extends Error {
300
286
  */
301
287
  constructor(currentTypoText: TypoText, source?: unknown) {
302
288
  const typoText = new TypoText();
303
- typoText.pushText(currentTypoText);
289
+ typoText.push(currentTypoText);
304
290
  if (source instanceof TypoError) {
305
- typoText.pushString(new TypoString(": "));
306
- typoText.pushText(source.#typoText);
291
+ typoText.push(new TypoString(": "));
292
+ typoText.push(source.#typoText);
307
293
  } else if (source instanceof Error) {
308
- typoText.pushString(new TypoString(`: ${source.message}`));
294
+ typoText.push(new TypoString(`: ${source.message}`));
309
295
  } else if (source !== undefined) {
310
- typoText.pushString(new TypoString(`: ${String(source)}`));
296
+ typoText.push(new TypoString(`: ${String(source)}`));
311
297
  }
312
298
  super(typoText.computeRawString());
313
299
  this.#typoText = typoText;
314
300
  }
315
301
  /**
316
- * Renders the styled error message (without a `"Error:"` prefix).
302
+ * Renders the styled message (without `"Error:"` prefix).
317
303
  *
318
304
  * @param typoSupport - Rendering mode.
319
305
  * @returns Styled error string.
@@ -322,8 +308,7 @@ export class TypoError extends Error {
322
308
  return this.#typoText.computeStyledString(typoSupport);
323
309
  }
324
310
  /**
325
- * Runs `thrower`; on any throw wraps it as a `TypoError` with `context()` prepended.
326
- * Useful for adding call-chain context (e.g. `"at 0: Number: ..."`).
311
+ * Runs `thrower`; wraps any thrown error as a `TypoError` with `context()` prepended.
327
312
  *
328
313
  * @typeParam Value - Return type of `thrower`.
329
314
  * @param thrower - Function to execute; result passed through on success.
@@ -344,9 +329,7 @@ export class TypoError extends Error {
344
329
  }
345
330
 
346
331
  /**
347
- * Controls ANSI terminal styling for {@link TypoString}, {@link TypoText}, and error rendering.
348
- * Create via {@link TypoSupport.none}, {@link TypoSupport.tty}, {@link TypoSupport.mock},
349
- * or {@link TypoSupport.inferFromProcess}.
332
+ * Controls ANSI terminal styling. Create via the static factory methods.
350
333
  */
351
334
  export class TypoSupport {
352
335
  #kind: "none" | "tty" | "mock";
@@ -354,49 +337,50 @@ export class TypoSupport {
354
337
  this.#kind = kind;
355
338
  }
356
339
  /**
357
- * Returns a `TypoSupport` that strips all styling (plain text, no ANSI codes).
340
+ * Plain text no ANSI codes.
358
341
  */
359
342
  static none(): TypoSupport {
360
343
  return new TypoSupport("none");
361
344
  }
362
345
  /**
363
- * Returns a `TypoSupport` that applies ANSI escape codes (for color-capable terminals).
346
+ * ANSI escape codes for color terminals.
364
347
  */
365
348
  static tty(): TypoSupport {
366
349
  return new TypoSupport("tty");
367
350
  }
368
351
  /**
369
- * Returns a `TypoSupport` with deterministic textual styling for snapshot tests.
370
- * Style flags appear as suffixes: `{text}@color`, `{text}+` (bold), `{text}-` (dim),
371
- * `{text}*` (italic), `{text}_` (underline), `{text}~` (strikethrough).
352
+ * Deterministic textual styling for snapshot tests.
372
353
  */
373
354
  static mock(): TypoSupport {
374
355
  return new TypoSupport("mock");
375
356
  }
376
357
  /**
377
- * Auto-detects styling mode from the process environment.
378
- * `FORCE_COLOR=0` / `NO_COLOR` → none; `FORCE_COLOR` (truthy) / `isTTY` → tty; else → none.
379
- * Falls back to none if `process` is unavailable.
358
+ * Auto-detects styling mode from the process environment on best-effort basis.
380
359
  */
381
- static inferFromProcess(): TypoSupport {
382
- if (!process) {
360
+ static inferFromEnv(): TypoSupport {
361
+ if (!process || !process.env) {
383
362
  return TypoSupport.none();
384
363
  }
385
- if (process.env) {
386
- if (process.env["FORCE_COLOR"] === "0") {
387
- return TypoSupport.none();
388
- }
389
- if (process.env["FORCE_COLOR"]) {
390
- return TypoSupport.tty();
391
- }
392
- if ("NO_COLOR" in process.env) {
393
- return TypoSupport.none();
364
+ function readEnvVar(name: string) {
365
+ if (!(name in process.env)) {
366
+ return undefined;
394
367
  }
368
+ return process.env[name];
395
369
  }
396
- if (process.stdout && process.stdout.isTTY) {
397
- return TypoSupport.tty();
370
+ const envForceColor = readEnvVar("FORCE_COLOR");
371
+ if (envForceColor === "0") {
372
+ return TypoSupport.none();
373
+ }
374
+ if (envForceColor !== undefined) {
375
+ TypoSupport.tty();
376
+ }
377
+ if (readEnvVar("NO_COLOR") !== undefined) {
378
+ return TypoSupport.none();
398
379
  }
399
- return TypoSupport.none();
380
+ if (readEnvVar("MOCK_COLOR") !== undefined) {
381
+ return TypoSupport.mock();
382
+ }
383
+ return TypoSupport.tty();
400
384
  }
401
385
  /**
402
386
  * Applies `typoStyle` to `value` according to the current mode.
@@ -406,8 +390,15 @@ export class TypoSupport {
406
390
  * @returns Styled string.
407
391
  */
408
392
  computeStyledString(value: string, typoStyle: TypoStyle): string {
393
+ let styledValue = value;
394
+ if (typoStyle.case === "upper") {
395
+ styledValue = styledValue.toUpperCase();
396
+ }
397
+ if (typoStyle.case === "lower") {
398
+ styledValue = styledValue.toLowerCase();
399
+ }
409
400
  if (this.#kind === "none") {
410
- return value;
401
+ return styledValue;
411
402
  }
412
403
  if (this.#kind === "tty") {
413
404
  const fgColorCode = typoStyle.fgColor
@@ -423,12 +414,12 @@ export class TypoSupport {
423
414
  const strikethroughCode = typoStyle.strikethrough
424
415
  ? ttyCodeStrikethrough
425
416
  : "";
426
- return `${fgColorCode}${bgColorCode}${boldCode}${dimCode}${italicCode}${underlineCode}${strikethroughCode}${value}${ttyCodeReset}`;
417
+ return `${fgColorCode}${bgColorCode}${boldCode}${dimCode}${italicCode}${underlineCode}${strikethroughCode}${styledValue}${ttyCodeReset}`;
427
418
  }
428
419
  if (this.#kind === "mock") {
429
420
  const fgColorPart = typoStyle.fgColor
430
- ? `{${value}}@${typoStyle.fgColor}`
431
- : value;
421
+ ? `{${styledValue}}@${typoStyle.fgColor}`
422
+ : styledValue;
432
423
  const bgColorPart = typoStyle.bgColor
433
424
  ? `{${fgColorPart}}#${typoStyle.bgColor}`
434
425
  : fgColorPart;