@runa-cmd/help 0.1.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.
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/dist/ansi/codes.d.ts +33 -0
- package/dist/ansi/codes.d.ts.map +1 -0
- package/dist/ansi/codes.js +54 -0
- package/dist/ansi/codes.js.map +1 -0
- package/dist/ansi/detect.d.ts +36 -0
- package/dist/ansi/detect.d.ts.map +1 -0
- package/dist/ansi/detect.js +82 -0
- package/dist/ansi/detect.js.map +1 -0
- package/dist/ansi/downsample.d.ts +22 -0
- package/dist/ansi/downsample.d.ts.map +1 -0
- package/dist/ansi/downsample.js +126 -0
- package/dist/ansi/downsample.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +63 -0
- package/dist/plugin.js.map +1 -0
- package/dist/primitives/box.d.ts +23 -0
- package/dist/primitives/box.d.ts.map +1 -0
- package/dist/primitives/box.js +76 -0
- package/dist/primitives/box.js.map +1 -0
- package/dist/primitives/columns.d.ts +15 -0
- package/dist/primitives/columns.d.ts.map +1 -0
- package/dist/primitives/columns.js +37 -0
- package/dist/primitives/columns.js.map +1 -0
- package/dist/primitives/join.d.ts +32 -0
- package/dist/primitives/join.d.ts.map +1 -0
- package/dist/primitives/join.js +89 -0
- package/dist/primitives/join.js.map +1 -0
- package/dist/primitives/list.d.ts +22 -0
- package/dist/primitives/list.d.ts.map +1 -0
- package/dist/primitives/list.js +25 -0
- package/dist/primitives/list.js.map +1 -0
- package/dist/primitives/styled.d.ts +28 -0
- package/dist/primitives/styled.d.ts.map +1 -0
- package/dist/primitives/styled.js +59 -0
- package/dist/primitives/styled.js.map +1 -0
- package/dist/primitives/table.d.ts +19 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +50 -0
- package/dist/primitives/table.js.map +1 -0
- package/dist/primitives/wrap.d.ts +10 -0
- package/dist/primitives/wrap.d.ts.map +1 -0
- package/dist/primitives/wrap.js +69 -0
- package/dist/primitives/wrap.js.map +1 -0
- package/dist/render.d.ts +8 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +219 -0
- package/dist/render.js.map +1 -0
- package/dist/theme.d.ts +12 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +45 -0
- package/dist/theme.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Runa CLI Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# @runa-cmd/help
|
|
2
|
+
|
|
3
|
+
Themeable help output for Runa CLIs with composable layout primitives.
|
|
4
|
+
|
|
5
|
+
Three layers of customization: zero-config defaults, theme colors, or build your own layout with primitives.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @runa-cmd/help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> Peer dependencies: `@runa-cmd/core` and `zod ^4.0.0`.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineCLI } from '@runa-cmd/core';
|
|
19
|
+
import { helpPlugin } from '@runa-cmd/help';
|
|
20
|
+
|
|
21
|
+
const cli = defineCLI({
|
|
22
|
+
meta: { name: 'my-tool', version: '1.0.0', description: 'My CLI tool' },
|
|
23
|
+
commands: { deploy, init },
|
|
24
|
+
plugins: [helpPlugin()],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
cli.run();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
$ my-tool --help
|
|
32
|
+
$ my-tool deploy --help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The plugin adds a `--help` / `-h` global flag. When triggered, it renders formatted help and exits.
|
|
36
|
+
|
|
37
|
+
## Three Layers
|
|
38
|
+
|
|
39
|
+
### Layer 1: Zero Config
|
|
40
|
+
|
|
41
|
+
Just add the plugin — sensible defaults with auto-detected terminal colors:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
plugins: [helpPlugin()]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Layer 2: Custom Theme
|
|
48
|
+
|
|
49
|
+
Override any of the 6 color slots:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
plugins: [
|
|
53
|
+
helpPlugin({
|
|
54
|
+
theme: {
|
|
55
|
+
primary: '#00BFFF', // Headings, command names
|
|
56
|
+
secondary: '#FF6B6B', // Flags, options
|
|
57
|
+
muted: 'dim', // Descriptions, defaults
|
|
58
|
+
error: '#FF0000', // Error messages
|
|
59
|
+
warning: '#FFAA00', // Deprecation notices
|
|
60
|
+
success: '#00FF00', // Success indicators
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Colors are automatically downsampled to match terminal capabilities: truecolor, 256-color, 16-color, or no color (`NO_COLOR` respected).
|
|
67
|
+
|
|
68
|
+
### Layer 3: Custom Renderer
|
|
69
|
+
|
|
70
|
+
Full control — use the layout primitives to build your own help output:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { helpPlugin, box, styled, table, join, bold, fg } from '@runa-cmd/help';
|
|
74
|
+
|
|
75
|
+
plugins: [
|
|
76
|
+
helpPlugin({
|
|
77
|
+
render({ schema, theme, termWidth }) {
|
|
78
|
+
return join.vertical([
|
|
79
|
+
box({
|
|
80
|
+
content: `${bold(schema.meta.name)} v${schema.meta.version}`,
|
|
81
|
+
border: 'rounded',
|
|
82
|
+
borderColor: theme.primary,
|
|
83
|
+
}),
|
|
84
|
+
styled(schema.meta.description ?? '', { color: theme.muted }),
|
|
85
|
+
table(
|
|
86
|
+
schema.commands.map((cmd) => [
|
|
87
|
+
styled(cmd.name, { bold: true, color: theme.primary }),
|
|
88
|
+
styled(cmd.description, { color: theme.muted }),
|
|
89
|
+
]),
|
|
90
|
+
{ headers: ['Command', 'Description'] },
|
|
91
|
+
),
|
|
92
|
+
]);
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Layout Primitives
|
|
99
|
+
|
|
100
|
+
All primitives return a `Block` (a plain string, possibly with ANSI codes). They're composable — the output of one can be the input to another.
|
|
101
|
+
|
|
102
|
+
### `box(options)`
|
|
103
|
+
|
|
104
|
+
Renders a bordered container with word wrapping.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { box } from '@runa-cmd/help';
|
|
108
|
+
|
|
109
|
+
box({
|
|
110
|
+
content: 'Hello from a box!',
|
|
111
|
+
border: 'rounded', // 'single' | 'double' | 'rounded' | 'none'
|
|
112
|
+
borderColor: '#7D56F4', // Hex or named ANSI color
|
|
113
|
+
padding: [1, 2], // [vertical, horizontal] or [top, right, bottom, left]
|
|
114
|
+
width: 40, // Fixed width (auto if omitted)
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `styled(text, options)`
|
|
119
|
+
|
|
120
|
+
Applies text formatting. Does not word-wrap.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { styled } from '@runa-cmd/help';
|
|
124
|
+
|
|
125
|
+
styled('Important!', {
|
|
126
|
+
bold: true,
|
|
127
|
+
italic: true,
|
|
128
|
+
color: '#FF6B6B',
|
|
129
|
+
padding: [0, 1],
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `columns(blocks, options)`
|
|
134
|
+
|
|
135
|
+
Distributes blocks evenly across terminal width:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { columns, styled } from '@runa-cmd/help';
|
|
139
|
+
|
|
140
|
+
columns([
|
|
141
|
+
styled('Left column'),
|
|
142
|
+
styled('Right column'),
|
|
143
|
+
], { gap: 4 });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `table(rows, options)`
|
|
147
|
+
|
|
148
|
+
Renders a 2D array as aligned columns with automatic width sizing:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { table, styled } from '@runa-cmd/help';
|
|
152
|
+
|
|
153
|
+
table([
|
|
154
|
+
[styled('--verbose', { bold: true }), 'Enable verbose output'],
|
|
155
|
+
[styled('--env', { bold: true }), 'Target environment'],
|
|
156
|
+
], {
|
|
157
|
+
padding: [0, 2],
|
|
158
|
+
headers: ['Flag', 'Description'],
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `list(items, options)`
|
|
163
|
+
|
|
164
|
+
Renders a bulleted list:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { list } from '@runa-cmd/help';
|
|
168
|
+
|
|
169
|
+
list(['First item', 'Second item', 'Third item'], {
|
|
170
|
+
bullet: '>', // Default: '•'
|
|
171
|
+
indent: 4, // Default: 2
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `join.vertical(blocks, options)` / `join.horizontal(blocks, options)`
|
|
176
|
+
|
|
177
|
+
Composes blocks together:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { join, box, styled } from '@runa-cmd/help';
|
|
181
|
+
|
|
182
|
+
// Stack vertically with gap
|
|
183
|
+
join.vertical([
|
|
184
|
+
styled('Header', { bold: true }),
|
|
185
|
+
styled('Body text'),
|
|
186
|
+
styled('Footer', { dim: true }),
|
|
187
|
+
], { gap: 1 });
|
|
188
|
+
|
|
189
|
+
// Place side by side
|
|
190
|
+
join.horizontal([
|
|
191
|
+
box({ content: 'Left', border: 'single' }),
|
|
192
|
+
box({ content: 'Right', border: 'single' }),
|
|
193
|
+
], { gap: 2, align: 'top' }); // 'top' | 'center' | 'bottom'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## ANSI Utilities
|
|
197
|
+
|
|
198
|
+
Low-level text formatting functions:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import {
|
|
202
|
+
bold, dim, italic, underline, // Text decorators
|
|
203
|
+
fg, reset, strip, // Color and cleanup
|
|
204
|
+
visibleWidth, // Width without ANSI codes
|
|
205
|
+
downsampleColor, // Hex → terminal-appropriate ANSI
|
|
206
|
+
detectColorDepth, // Terminal color capability
|
|
207
|
+
detectTermWidth, // Terminal width (default 80)
|
|
208
|
+
getRenderContext, // Lazy-detected { colorDepth, termWidth }
|
|
209
|
+
resetRenderContext, // Clear cached detection (for tests)
|
|
210
|
+
} from '@runa-cmd/help';
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Color Downsampling
|
|
214
|
+
|
|
215
|
+
Colors automatically adapt to terminal capabilities:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { downsampleColor } from '@runa-cmd/help';
|
|
219
|
+
|
|
220
|
+
downsampleColor('#7D56F4', 'truecolor'); // '\x1b[38;2;125;86;244m'
|
|
221
|
+
downsampleColor('#7D56F4', '256'); // '\x1b[38;5;99m'
|
|
222
|
+
downsampleColor('#7D56F4', '16'); // '\x1b[35m' (magenta)
|
|
223
|
+
downsampleColor('#7D56F4', 'none'); // '' (no color)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Respects `NO_COLOR`, `FORCE_COLOR`, and `COLORTERM` environment variables.
|
|
227
|
+
|
|
228
|
+
## Theme
|
|
229
|
+
|
|
230
|
+
The default theme:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { defaultTheme, resolveTheme } from '@runa-cmd/help';
|
|
234
|
+
|
|
235
|
+
// defaultTheme = {
|
|
236
|
+
// primary: '#7D56F4', // Lipgloss purple
|
|
237
|
+
// secondary: '#FF6B6B',
|
|
238
|
+
// muted: 'dim',
|
|
239
|
+
// error: '#FF0000',
|
|
240
|
+
// warning: '#FFAA00',
|
|
241
|
+
// success: '#00FF00',
|
|
242
|
+
// }
|
|
243
|
+
|
|
244
|
+
// Resolve a partial override to ANSI-ready strings:
|
|
245
|
+
const resolved = resolveTheme({ primary: '#00BFFF' }, getRenderContext());
|
|
246
|
+
// resolved.primary = '\x1b[38;2;0;191;255m' (on truecolor terminals)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Default Renderer
|
|
250
|
+
|
|
251
|
+
The built-in renderer (`defaultRenderer`) produces output with these sections (each skipped if empty):
|
|
252
|
+
|
|
253
|
+
1. **Header** — Name, version, description
|
|
254
|
+
2. **Usage** — `name [command] [options]`
|
|
255
|
+
3. **Arguments** — Positional args table
|
|
256
|
+
4. **Options** — Named options table, grouped by `group` metadata
|
|
257
|
+
5. **Commands** — Subcommands table
|
|
258
|
+
|
|
259
|
+
The renderer is subcommand-aware: `my-tool deploy --help` shows help for the `deploy` command specifically.
|
|
260
|
+
|
|
261
|
+
## Types
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import type {
|
|
265
|
+
HelpPluginOptions,
|
|
266
|
+
RenderFnCtx,
|
|
267
|
+
Theme, ResolvedTheme,
|
|
268
|
+
Block,
|
|
269
|
+
RenderContext, ColorDepth,
|
|
270
|
+
BoxOptions, StyledOptions, ColumnsOptions,
|
|
271
|
+
TableOptions, ListOptions,
|
|
272
|
+
VerticalJoinOptions, HorizontalJoinOptions, HorizontalAlign,
|
|
273
|
+
BorderStyle,
|
|
274
|
+
} from '@runa-cmd/help';
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @runa-cmd/help — ANSI escape code builders
|
|
3
|
+
*
|
|
4
|
+
* Low-level helpers for applying text styles via ANSI escape sequences.
|
|
5
|
+
* These produce raw escape strings — color downsampling is handled by downsample.ts.
|
|
6
|
+
*/
|
|
7
|
+
/** ANSI reset — clears all styles */
|
|
8
|
+
export declare const reset = "\u001B[0m";
|
|
9
|
+
/** Apply bold style */
|
|
10
|
+
export declare function bold(text: string): string;
|
|
11
|
+
/** Apply dim style */
|
|
12
|
+
export declare function dim(text: string): string;
|
|
13
|
+
/** Apply italic style */
|
|
14
|
+
export declare function italic(text: string): string;
|
|
15
|
+
/** Apply underline style */
|
|
16
|
+
export declare function underline(text: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Apply foreground color.
|
|
19
|
+
* Accepts a pre-computed ANSI escape string (from downsample.ts).
|
|
20
|
+
* If empty string (no color mode), returns text unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fg(text: string, ansiColor: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Strip all ANSI escape codes from a string.
|
|
25
|
+
* Useful for raw mode testing and width calculations.
|
|
26
|
+
*/
|
|
27
|
+
export declare function strip(text: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Get the visible width of a string (excluding ANSI codes).
|
|
30
|
+
* Note: does NOT handle Unicode wide characters (CJK, emoji) — deferred to future package.
|
|
31
|
+
*/
|
|
32
|
+
export declare function visibleWidth(text: string): number;
|
|
33
|
+
//# sourceMappingURL=codes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../../src/ansi/codes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,qCAAqC;AACrC,eAAO,MAAM,KAAK,cAAa,CAAC;AAEhC,uBAAuB;AACvB,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,sBAAsB;AACtB,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExC;AAED,yBAAyB;AACzB,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG1D;AAQD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @runa-cmd/help — ANSI escape code builders
|
|
3
|
+
*
|
|
4
|
+
* Low-level helpers for applying text styles via ANSI escape sequences.
|
|
5
|
+
* These produce raw escape strings — color downsampling is handled by downsample.ts.
|
|
6
|
+
*/
|
|
7
|
+
const ESC = '\x1b[';
|
|
8
|
+
/** ANSI reset — clears all styles */
|
|
9
|
+
export const reset = `${ESC}0m`;
|
|
10
|
+
/** Apply bold style */
|
|
11
|
+
export function bold(text) {
|
|
12
|
+
return `${ESC}1m${text}${reset}`;
|
|
13
|
+
}
|
|
14
|
+
/** Apply dim style */
|
|
15
|
+
export function dim(text) {
|
|
16
|
+
return `${ESC}2m${text}${reset}`;
|
|
17
|
+
}
|
|
18
|
+
/** Apply italic style */
|
|
19
|
+
export function italic(text) {
|
|
20
|
+
return `${ESC}3m${text}${reset}`;
|
|
21
|
+
}
|
|
22
|
+
/** Apply underline style */
|
|
23
|
+
export function underline(text) {
|
|
24
|
+
return `${ESC}4m${text}${reset}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Apply foreground color.
|
|
28
|
+
* Accepts a pre-computed ANSI escape string (from downsample.ts).
|
|
29
|
+
* If empty string (no color mode), returns text unchanged.
|
|
30
|
+
*/
|
|
31
|
+
export function fg(text, ansiColor) {
|
|
32
|
+
if (!ansiColor)
|
|
33
|
+
return text;
|
|
34
|
+
return `${ansiColor}${text}${reset}`;
|
|
35
|
+
}
|
|
36
|
+
// ─── ANSI stripping regex ───────────────────────────────────
|
|
37
|
+
// Matches all ANSI escape sequences (CSI sequences + OSC sequences)
|
|
38
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape detection requires control characters
|
|
39
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
40
|
+
/**
|
|
41
|
+
* Strip all ANSI escape codes from a string.
|
|
42
|
+
* Useful for raw mode testing and width calculations.
|
|
43
|
+
*/
|
|
44
|
+
export function strip(text) {
|
|
45
|
+
return text.replace(ANSI_RE, '');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the visible width of a string (excluding ANSI codes).
|
|
49
|
+
* Note: does NOT handle Unicode wide characters (CJK, emoji) — deferred to future package.
|
|
50
|
+
*/
|
|
51
|
+
export function visibleWidth(text) {
|
|
52
|
+
return strip(text).length;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.js","sourceRoot":"","sources":["../../src/ansi/codes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,GAAG,GAAG,OAAO,CAAC;AAEpB,qCAAqC;AACrC,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,GAAG,IAAI,CAAC;AAEhC,uBAAuB;AACvB,MAAM,UAAU,IAAI,CAAC,IAAY;IAChC,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED,sBAAsB;AACtB,MAAM,UAAU,GAAG,CAAC,IAAY;IAC/B,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED,yBAAyB;AACzB,MAAM,UAAU,MAAM,CAAC,IAAY;IAClC,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,SAAS,CAAC,IAAY;IACrC,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,EAAE,CAAC,IAAY,EAAE,SAAiB;IACjD,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO,GAAG,SAAS,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC;AACtC,CAAC;AAED,+DAA+D;AAE/D,oEAAoE;AACpE,6GAA6G;AAC7G,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @runa-cmd/help — Terminal detection
|
|
3
|
+
*
|
|
4
|
+
* Detects terminal color depth and width from environment variables
|
|
5
|
+
* and process.stdout capabilities.
|
|
6
|
+
*/
|
|
7
|
+
import type { ColorDepth, RenderContext } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Detect terminal color depth from env vars and stdout capabilities.
|
|
10
|
+
*
|
|
11
|
+
* Priority order:
|
|
12
|
+
* 1. NO_COLOR → 'none'
|
|
13
|
+
* 2. FORCE_COLOR → minimum '16'
|
|
14
|
+
* 3. COLORTERM=truecolor|24bit → 'truecolor'
|
|
15
|
+
* 4. TERM=xterm-256color → '256'
|
|
16
|
+
* 5. process.stdout.hasColors(256) → '256'
|
|
17
|
+
* 6. process.stdout.hasColors(16) → '16'
|
|
18
|
+
* 7. fallback → 'none'
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectColorDepth(): ColorDepth;
|
|
21
|
+
/**
|
|
22
|
+
* Detect terminal width in columns.
|
|
23
|
+
* Falls back to 80 if unavailable (e.g. piped output).
|
|
24
|
+
*/
|
|
25
|
+
export declare function detectTermWidth(): number;
|
|
26
|
+
/**
|
|
27
|
+
* Get the current RenderContext (lazy singleton — auto-detected once).
|
|
28
|
+
* All primitives use this internally when no explicit ctx is passed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getRenderContext(): RenderContext;
|
|
31
|
+
/**
|
|
32
|
+
* Reset the cached RenderContext.
|
|
33
|
+
* Used in tests to force re-detection.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resetRenderContext(): void;
|
|
36
|
+
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/ansi/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,IAAI,UAAU,CAyC7C;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAQhD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect terminal color depth from env vars and stdout capabilities.
|
|
3
|
+
*
|
|
4
|
+
* Priority order:
|
|
5
|
+
* 1. NO_COLOR → 'none'
|
|
6
|
+
* 2. FORCE_COLOR → minimum '16'
|
|
7
|
+
* 3. COLORTERM=truecolor|24bit → 'truecolor'
|
|
8
|
+
* 4. TERM=xterm-256color → '256'
|
|
9
|
+
* 5. process.stdout.hasColors(256) → '256'
|
|
10
|
+
* 6. process.stdout.hasColors(16) → '16'
|
|
11
|
+
* 7. fallback → 'none'
|
|
12
|
+
*/
|
|
13
|
+
export function detectColorDepth() {
|
|
14
|
+
const env = process.env;
|
|
15
|
+
// NO_COLOR takes absolute priority (https://no-color.org/)
|
|
16
|
+
if (env.NO_COLOR !== undefined) {
|
|
17
|
+
return 'none';
|
|
18
|
+
}
|
|
19
|
+
// FORCE_COLOR forces color output (e.g. in CI or piped contexts)
|
|
20
|
+
const forceColor = env.FORCE_COLOR;
|
|
21
|
+
if (forceColor !== undefined) {
|
|
22
|
+
if (forceColor === '3')
|
|
23
|
+
return 'truecolor';
|
|
24
|
+
if (forceColor === '2')
|
|
25
|
+
return '256';
|
|
26
|
+
return '16';
|
|
27
|
+
}
|
|
28
|
+
// Non-TTY (piped) → no color unless FORCE_COLOR was set (handled above)
|
|
29
|
+
if (!process.stdout.isTTY) {
|
|
30
|
+
return 'none';
|
|
31
|
+
}
|
|
32
|
+
// COLORTERM for truecolor detection
|
|
33
|
+
const colorTerm = env.COLORTERM;
|
|
34
|
+
if (colorTerm === 'truecolor' || colorTerm === '24bit') {
|
|
35
|
+
return 'truecolor';
|
|
36
|
+
}
|
|
37
|
+
// TERM for 256 color detection
|
|
38
|
+
const term = env.TERM;
|
|
39
|
+
if (term?.includes('256color')) {
|
|
40
|
+
return '256';
|
|
41
|
+
}
|
|
42
|
+
// process.stdout.hasColors() as additional signal
|
|
43
|
+
if (typeof process.stdout.hasColors === 'function') {
|
|
44
|
+
if (process.stdout.hasColors(16_777_216))
|
|
45
|
+
return 'truecolor';
|
|
46
|
+
if (process.stdout.hasColors(256))
|
|
47
|
+
return '256';
|
|
48
|
+
if (process.stdout.hasColors(16))
|
|
49
|
+
return '16';
|
|
50
|
+
}
|
|
51
|
+
return 'none';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Detect terminal width in columns.
|
|
55
|
+
* Falls back to 80 if unavailable (e.g. piped output).
|
|
56
|
+
*/
|
|
57
|
+
export function detectTermWidth() {
|
|
58
|
+
return process.stdout.columns ?? 80;
|
|
59
|
+
}
|
|
60
|
+
// ─── Lazy Singleton ─────────────────────────────────────────
|
|
61
|
+
let _cached;
|
|
62
|
+
/**
|
|
63
|
+
* Get the current RenderContext (lazy singleton — auto-detected once).
|
|
64
|
+
* All primitives use this internally when no explicit ctx is passed.
|
|
65
|
+
*/
|
|
66
|
+
export function getRenderContext() {
|
|
67
|
+
if (!_cached) {
|
|
68
|
+
_cached = {
|
|
69
|
+
colorDepth: detectColorDepth(),
|
|
70
|
+
termWidth: detectTermWidth(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return _cached;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reset the cached RenderContext.
|
|
77
|
+
* Used in tests to force re-detection.
|
|
78
|
+
*/
|
|
79
|
+
export function resetRenderContext() {
|
|
80
|
+
_cached = undefined;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../../src/ansi/detect.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB;IAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IAExB,2DAA2D;IAC3D,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,MAAM,CAAC;IACf,CAAC;IAED,iEAAiE;IACjE,MAAM,UAAU,GAAG,GAAG,CAAC,WAAW,CAAC;IACnC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,WAAW,CAAC;QAC3C,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,KAAK,CAAC;QACrC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,wEAAwE;IACxE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC;IACf,CAAC;IAED,oCAAoC;IACpC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IAChC,IAAI,SAAS,KAAK,WAAW,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;QACxD,OAAO,WAAW,CAAC;IACpB,CAAC;IAED,+BAA+B;IAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;IACtB,IAAI,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACd,CAAC;IAED,kDAAkD;IAClD,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YAAE,OAAO,WAAW,CAAC;QAC7D,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;IAC/C,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC9B,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;AACrC,CAAC;AAED,+DAA+D;AAE/D,IAAI,OAAkC,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC/B,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,GAAG;YACT,UAAU,EAAE,gBAAgB,EAAE;YAC9B,SAAS,EAAE,eAAe,EAAE;SAC5B,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IACjC,OAAO,GAAG,SAAS,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @runa-cmd/help — Color downsampling
|
|
3
|
+
*
|
|
4
|
+
* Converts hex colors (#RRGGBB) to the appropriate ANSI escape sequence
|
|
5
|
+
* based on the terminal's color depth. Named attributes (e.g. 'dim')
|
|
6
|
+
* pass through unchanged.
|
|
7
|
+
*
|
|
8
|
+
* ANSI 256 palette: 6×6×6 RGB cube (indices 16-231) + 24 grayscale (232-255).
|
|
9
|
+
* Computed programmatically at module load, not hardcoded.
|
|
10
|
+
*/
|
|
11
|
+
import type { ColorDepth } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Downsample a color to an ANSI escape string based on terminal color depth.
|
|
14
|
+
*
|
|
15
|
+
* - Hex colors (#RRGGBB) → converted to nearest ANSI code for the depth
|
|
16
|
+
* - Named attributes ('dim', 'red', etc.) → passed through as ANSI escape
|
|
17
|
+
* - If depth is 'none' → returns '' (empty, no color)
|
|
18
|
+
*
|
|
19
|
+
* Returns the ANSI escape STRING (without reset) — caller wraps text with it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function downsampleColor(color: string, depth: ColorDepth): string;
|
|
22
|
+
//# sourceMappingURL=downsample.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"downsample.d.ts","sourceRoot":"","sources":["../../src/ansi/downsample.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA8G9C;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAiCxE"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ─── Hex Parsing ────────────────────────────────────────────
|
|
2
|
+
const HEX_RE = /^#([0-9a-f]{6})$/i;
|
|
3
|
+
function parseHex(hex) {
|
|
4
|
+
const match = HEX_RE.exec(hex);
|
|
5
|
+
if (!match)
|
|
6
|
+
return null;
|
|
7
|
+
const val = Number.parseInt(match[1], 16);
|
|
8
|
+
return [(val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff];
|
|
9
|
+
}
|
|
10
|
+
const PALETTE_256 = [];
|
|
11
|
+
// Standard 16 colors (indices 0-15)
|
|
12
|
+
const STANDARD_16 = [
|
|
13
|
+
[0, 0, 0], // 0: black
|
|
14
|
+
[128, 0, 0], // 1: red
|
|
15
|
+
[0, 128, 0], // 2: green
|
|
16
|
+
[128, 128, 0], // 3: yellow
|
|
17
|
+
[0, 0, 128], // 4: blue
|
|
18
|
+
[128, 0, 128], // 5: magenta
|
|
19
|
+
[0, 128, 128], // 6: cyan
|
|
20
|
+
[192, 192, 192], // 7: white
|
|
21
|
+
[128, 128, 128], // 8: bright black (gray)
|
|
22
|
+
[255, 0, 0], // 9: bright red
|
|
23
|
+
[0, 255, 0], // 10: bright green
|
|
24
|
+
[255, 255, 0], // 11: bright yellow
|
|
25
|
+
[0, 0, 255], // 12: bright blue
|
|
26
|
+
[255, 0, 255], // 13: bright magenta
|
|
27
|
+
[0, 255, 255], // 14: bright cyan
|
|
28
|
+
[255, 255, 255], // 15: bright white
|
|
29
|
+
];
|
|
30
|
+
// Build standard 16
|
|
31
|
+
for (let i = 0; i < 16; i++) {
|
|
32
|
+
const [r, g, b] = STANDARD_16[i];
|
|
33
|
+
PALETTE_256.push({ r, g, b, index: i });
|
|
34
|
+
}
|
|
35
|
+
// 6×6×6 RGB cube (indices 16-231)
|
|
36
|
+
const CUBE_STEPS = [0, 95, 135, 175, 215, 255];
|
|
37
|
+
for (let ri = 0; ri < 6; ri++) {
|
|
38
|
+
for (let gi = 0; gi < 6; gi++) {
|
|
39
|
+
for (let bi = 0; bi < 6; bi++) {
|
|
40
|
+
PALETTE_256.push({
|
|
41
|
+
r: CUBE_STEPS[ri],
|
|
42
|
+
g: CUBE_STEPS[gi],
|
|
43
|
+
b: CUBE_STEPS[bi],
|
|
44
|
+
index: 16 + ri * 36 + gi * 6 + bi,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Grayscale ramp (indices 232-255)
|
|
50
|
+
for (let i = 0; i < 24; i++) {
|
|
51
|
+
const v = 8 + i * 10;
|
|
52
|
+
PALETTE_256.push({ r: v, g: v, b: v, index: 232 + i });
|
|
53
|
+
}
|
|
54
|
+
// ─── Nearest Color Search ───────────────────────────────────
|
|
55
|
+
function nearestIndex(r, g, b, palette) {
|
|
56
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
57
|
+
let bestIndex = 0;
|
|
58
|
+
for (const entry of palette) {
|
|
59
|
+
const dr = r - entry.r;
|
|
60
|
+
const dg = g - entry.g;
|
|
61
|
+
const db = b - entry.b;
|
|
62
|
+
const dist = dr * dr + dg * dg + db * db;
|
|
63
|
+
if (dist < bestDist) {
|
|
64
|
+
bestDist = dist;
|
|
65
|
+
bestIndex = entry.index;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return bestIndex;
|
|
69
|
+
}
|
|
70
|
+
// ─── Named ANSI Attributes ─────────────────────────────────
|
|
71
|
+
const ESC = '\x1b[';
|
|
72
|
+
const NAMED_ATTRS = {
|
|
73
|
+
bold: `${ESC}1m`,
|
|
74
|
+
dim: `${ESC}2m`,
|
|
75
|
+
italic: `${ESC}3m`,
|
|
76
|
+
underline: `${ESC}4m`,
|
|
77
|
+
red: `${ESC}31m`,
|
|
78
|
+
green: `${ESC}32m`,
|
|
79
|
+
yellow: `${ESC}33m`,
|
|
80
|
+
blue: `${ESC}34m`,
|
|
81
|
+
magenta: `${ESC}35m`,
|
|
82
|
+
cyan: `${ESC}36m`,
|
|
83
|
+
white: `${ESC}37m`,
|
|
84
|
+
};
|
|
85
|
+
// ─── Public API ─────────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* Downsample a color to an ANSI escape string based on terminal color depth.
|
|
88
|
+
*
|
|
89
|
+
* - Hex colors (#RRGGBB) → converted to nearest ANSI code for the depth
|
|
90
|
+
* - Named attributes ('dim', 'red', etc.) → passed through as ANSI escape
|
|
91
|
+
* - If depth is 'none' → returns '' (empty, no color)
|
|
92
|
+
*
|
|
93
|
+
* Returns the ANSI escape STRING (without reset) — caller wraps text with it.
|
|
94
|
+
*/
|
|
95
|
+
export function downsampleColor(color, depth) {
|
|
96
|
+
if (depth === 'none')
|
|
97
|
+
return '';
|
|
98
|
+
// Named attribute passthrough
|
|
99
|
+
const named = NAMED_ATTRS[color];
|
|
100
|
+
if (named)
|
|
101
|
+
return named;
|
|
102
|
+
// Parse hex
|
|
103
|
+
const rgb = parseHex(color);
|
|
104
|
+
if (!rgb)
|
|
105
|
+
return '';
|
|
106
|
+
const [r, g, b] = rgb;
|
|
107
|
+
switch (depth) {
|
|
108
|
+
case 'truecolor':
|
|
109
|
+
return `${ESC}38;2;${r};${g};${b}m`;
|
|
110
|
+
case '256': {
|
|
111
|
+
// Search full 256 palette (16 + cube + grayscale)
|
|
112
|
+
const idx = nearestIndex(r, g, b, PALETTE_256);
|
|
113
|
+
return `${ESC}38;5;${idx}m`;
|
|
114
|
+
}
|
|
115
|
+
case '16': {
|
|
116
|
+
// Search only the standard 16 colors
|
|
117
|
+
const idx = nearestIndex(r, g, b, PALETTE_256.slice(0, 16));
|
|
118
|
+
if (idx >= 8) {
|
|
119
|
+
// Bright colors: use 90-97 range
|
|
120
|
+
return `${ESC}${90 + (idx - 8)}m`;
|
|
121
|
+
}
|
|
122
|
+
return `${ESC}${30 + idx}m`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=downsample.js.map
|