@rlabs-inc/tui 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/README.md +141 -0
- package/index.ts +45 -0
- package/package.json +59 -0
- package/src/api/index.ts +7 -0
- package/src/api/mount.ts +230 -0
- package/src/engine/arrays/core.ts +60 -0
- package/src/engine/arrays/dimensions.ts +68 -0
- package/src/engine/arrays/index.ts +166 -0
- package/src/engine/arrays/interaction.ts +112 -0
- package/src/engine/arrays/layout.ts +175 -0
- package/src/engine/arrays/spacing.ts +100 -0
- package/src/engine/arrays/text.ts +55 -0
- package/src/engine/arrays/visual.ts +140 -0
- package/src/engine/index.ts +25 -0
- package/src/engine/inheritance.ts +138 -0
- package/src/engine/registry.ts +180 -0
- package/src/pipeline/frameBuffer.ts +473 -0
- package/src/pipeline/layout/index.ts +105 -0
- package/src/pipeline/layout/titan-engine.ts +798 -0
- package/src/pipeline/layout/types.ts +194 -0
- package/src/pipeline/layout/utils/hierarchy.ts +202 -0
- package/src/pipeline/layout/utils/math.ts +134 -0
- package/src/pipeline/layout/utils/text-measure.ts +160 -0
- package/src/pipeline/layout.ts +30 -0
- package/src/primitives/box.ts +312 -0
- package/src/primitives/index.ts +12 -0
- package/src/primitives/text.ts +199 -0
- package/src/primitives/types.ts +222 -0
- package/src/primitives/utils.ts +37 -0
- package/src/renderer/ansi.ts +625 -0
- package/src/renderer/buffer.ts +667 -0
- package/src/renderer/index.ts +40 -0
- package/src/renderer/input.ts +518 -0
- package/src/renderer/output.ts +451 -0
- package/src/state/cursor.ts +176 -0
- package/src/state/focus.ts +241 -0
- package/src/state/index.ts +43 -0
- package/src/state/keyboard.ts +771 -0
- package/src/state/mouse.ts +524 -0
- package/src/state/scroll.ts +341 -0
- package/src/state/theme.ts +687 -0
- package/src/types/color.ts +401 -0
- package/src/types/index.ts +316 -0
- package/src/utils/text.ts +471 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Color Utilities
|
|
3
|
+
*
|
|
4
|
+
* All color parsing, conversion, and manipulation.
|
|
5
|
+
* Uses Bun.color() for parsing CSS colors and ANSI conversion.
|
|
6
|
+
* Includes OKLCH support for perceptually uniform color manipulation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RGBA, CellAttrs } from './index'
|
|
10
|
+
import { Attr } from './index'
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Color Presets
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export const Colors = {
|
|
17
|
+
BLACK: { r: 0, g: 0, b: 0, a: 255 } as RGBA,
|
|
18
|
+
WHITE: { r: 255, g: 255, b: 255, a: 255 } as RGBA,
|
|
19
|
+
RED: { r: 255, g: 0, b: 0, a: 255 } as RGBA,
|
|
20
|
+
GREEN: { r: 0, g: 255, b: 0, a: 255 } as RGBA,
|
|
21
|
+
BLUE: { r: 0, g: 0, b: 255, a: 255 } as RGBA,
|
|
22
|
+
YELLOW: { r: 255, g: 255, b: 0, a: 255 } as RGBA,
|
|
23
|
+
CYAN: { r: 0, g: 255, b: 255, a: 255 } as RGBA,
|
|
24
|
+
MAGENTA: { r: 255, g: 0, b: 255, a: 255 } as RGBA,
|
|
25
|
+
GRAY: { r: 128, g: 128, b: 128, a: 255 } as RGBA,
|
|
26
|
+
TRANSPARENT: { r: 0, g: 0, b: 0, a: 0 } as RGBA,
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Terminal default color marker.
|
|
31
|
+
* When used, the renderer emits reset code instead of color.
|
|
32
|
+
* Uses a: 255 so it's treated as "visible" (not transparent).
|
|
33
|
+
* The r: -1 marker is how we identify it as "use terminal default".
|
|
34
|
+
*/
|
|
35
|
+
export const TERMINAL_DEFAULT: RGBA = { r: -1, g: -1, b: -1, a: 255 }
|
|
36
|
+
|
|
37
|
+
export function isTerminalDefault(color: RGBA): boolean {
|
|
38
|
+
return color.r === -1 && color.g === -1 && color.b === -1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ANSI color marker.
|
|
43
|
+
* When r = -2, the `g` field contains the ANSI color index (0-255).
|
|
44
|
+
* The renderer will output proper ANSI escape codes that respect
|
|
45
|
+
* the user's terminal color palette.
|
|
46
|
+
*
|
|
47
|
+
* - 0-7: Standard colors (black, red, green, yellow, blue, magenta, cyan, white)
|
|
48
|
+
* - 8-15: Bright colors
|
|
49
|
+
* - 16-231: 6x6x6 color cube
|
|
50
|
+
* - 232-255: Grayscale
|
|
51
|
+
*/
|
|
52
|
+
export function ansiColor(index: number): RGBA {
|
|
53
|
+
return { r: -2, g: index, b: 0, a: 255 }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isAnsiColor(color: RGBA): boolean {
|
|
57
|
+
return color.r === -2
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getAnsiIndex(color: RGBA): number {
|
|
61
|
+
return color.g
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Color Creation
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
/** Create an RGBA color */
|
|
69
|
+
export function rgba(r: number, g: number, b: number, a: number = 255): RGBA {
|
|
70
|
+
return { r, g, b, a }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// Color Parsing - Bun.color() integration
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse any color input to RGBA.
|
|
79
|
+
* Supports: hex, rgb(), rgba(), hsl(), CSS names, oklch(), etc.
|
|
80
|
+
*/
|
|
81
|
+
export function parseColor(input: string | number | RGBA): RGBA {
|
|
82
|
+
// Already RGBA
|
|
83
|
+
if (typeof input === 'object' && 'r' in input) {
|
|
84
|
+
return { r: input.r, g: input.g, b: input.b, a: input.a ?? 255 }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Integer color (0xRRGGBB)
|
|
88
|
+
if (typeof input === 'number') {
|
|
89
|
+
return {
|
|
90
|
+
r: (input >> 16) & 0xff,
|
|
91
|
+
g: (input >> 8) & 0xff,
|
|
92
|
+
b: input & 0xff,
|
|
93
|
+
a: 255,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const trimmed = input.trim().toLowerCase()
|
|
98
|
+
|
|
99
|
+
// Handle special values
|
|
100
|
+
if (!trimmed || trimmed === 'transparent') {
|
|
101
|
+
return Colors.TRANSPARENT
|
|
102
|
+
}
|
|
103
|
+
if (trimmed === 'inherit' || trimmed === 'initial' || trimmed === 'currentcolor') {
|
|
104
|
+
return TERMINAL_DEFAULT
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// OKLCH - Bun.color() doesn't support yet, parse manually
|
|
108
|
+
if (trimmed.startsWith('oklch(')) {
|
|
109
|
+
return parseOklch(trimmed) ?? Colors.MAGENTA
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Use Bun.color() for everything else
|
|
113
|
+
const result = Bun.color(input, '{rgba}')
|
|
114
|
+
if (result) {
|
|
115
|
+
return {
|
|
116
|
+
r: Math.round(result.r),
|
|
117
|
+
g: Math.round(result.g),
|
|
118
|
+
b: Math.round(result.b),
|
|
119
|
+
a: Math.round((result.a ?? 1) * 255),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback to magenta (visible error indicator)
|
|
124
|
+
return Colors.MAGENTA
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// OKLCH Support
|
|
129
|
+
// =============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse OKLCH color string.
|
|
133
|
+
* Format: oklch(L C H) or oklch(L C H / A)
|
|
134
|
+
* - L: Lightness 0-1 (or 0%-100%)
|
|
135
|
+
* - C: Chroma 0-0.4 roughly
|
|
136
|
+
* - H: Hue 0-360 degrees
|
|
137
|
+
*/
|
|
138
|
+
function parseOklch(value: string): RGBA | null {
|
|
139
|
+
const match = value.match(/^oklch\s*\(\s*([^)]+)\s*\)$/)
|
|
140
|
+
if (!match) return null
|
|
141
|
+
|
|
142
|
+
const parts = match[1]!.split(/[\s/]+/).filter(Boolean)
|
|
143
|
+
if (parts.length < 3) return null
|
|
144
|
+
|
|
145
|
+
// Parse L (lightness)
|
|
146
|
+
let l = parseFloat(parts[0]!)
|
|
147
|
+
if (parts[0]!.endsWith('%')) {
|
|
148
|
+
l = parseFloat(parts[0]!) / 100
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Parse C (chroma)
|
|
152
|
+
const c = parseFloat(parts[1]!)
|
|
153
|
+
|
|
154
|
+
// Parse H (hue)
|
|
155
|
+
let h = parseFloat(parts[2]!)
|
|
156
|
+
if (parts[2]!.endsWith('rad')) {
|
|
157
|
+
h = parseFloat(parts[2]!) * (180 / Math.PI)
|
|
158
|
+
} else if (parts[2]!.endsWith('turn')) {
|
|
159
|
+
h = parseFloat(parts[2]!) * 360
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Parse A (alpha) if present
|
|
163
|
+
let a = 255
|
|
164
|
+
if (parts.length > 3) {
|
|
165
|
+
const alphaValue = parseFloat(parts[3]!)
|
|
166
|
+
a = parts[3]!.endsWith('%')
|
|
167
|
+
? Math.round((alphaValue / 100) * 255)
|
|
168
|
+
: Math.round(alphaValue * 255)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isNaN(l) || isNaN(c) || isNaN(h)) return null
|
|
172
|
+
|
|
173
|
+
const rgb = oklchToRgb(l, c, h)
|
|
174
|
+
return { ...rgb, a }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create an RGBA color from OKLCH values.
|
|
179
|
+
* Perfect for perceptually uniform gradients!
|
|
180
|
+
*
|
|
181
|
+
* @param l Lightness (0-1)
|
|
182
|
+
* @param c Chroma (0-0.4 roughly, 0.15 is good for vivid colors)
|
|
183
|
+
* @param h Hue (0-360 degrees)
|
|
184
|
+
* @param a Alpha (0-255, default 255)
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* // Beautiful rainbow gradient
|
|
188
|
+
* for (let x = 0; x < width; x++) {
|
|
189
|
+
* const hue = (x / width) * 360;
|
|
190
|
+
* const color = oklch(0.7, 0.15, hue);
|
|
191
|
+
* // Use color...
|
|
192
|
+
* }
|
|
193
|
+
*/
|
|
194
|
+
export function oklch(l: number, c: number, h: number, a: number = 255): RGBA {
|
|
195
|
+
const rgb = oklchToRgb(l, c, h)
|
|
196
|
+
return { ...rgb, a }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert OKLCH to RGB.
|
|
201
|
+
* Based on CSS Color Level 4 specification.
|
|
202
|
+
*/
|
|
203
|
+
function oklchToRgb(l: number, c: number, h: number): { r: number; g: number; b: number } {
|
|
204
|
+
// OKLCH to OKLab
|
|
205
|
+
const hRad = (h * Math.PI) / 180
|
|
206
|
+
const a = c * Math.cos(hRad)
|
|
207
|
+
const b = c * Math.sin(hRad)
|
|
208
|
+
|
|
209
|
+
// OKLab to linear sRGB via LMS
|
|
210
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * b
|
|
211
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * b
|
|
212
|
+
const s_ = l - 0.0894841775 * a - 1.2914855480 * b
|
|
213
|
+
|
|
214
|
+
const lCubed = l_ * l_ * l_
|
|
215
|
+
const mCubed = m_ * m_ * m_
|
|
216
|
+
const sCubed = s_ * s_ * s_
|
|
217
|
+
|
|
218
|
+
const rLinear = +4.0767416621 * lCubed - 3.3077115913 * mCubed + 0.2309699292 * sCubed
|
|
219
|
+
const gLinear = -1.2684380046 * lCubed + 2.6097574011 * mCubed - 0.3413193965 * sCubed
|
|
220
|
+
const bLinear = -0.0041960863 * lCubed - 0.7034186147 * mCubed + 1.7076147010 * sCubed
|
|
221
|
+
|
|
222
|
+
// Linear sRGB to sRGB (gamma correction)
|
|
223
|
+
const toSrgb = (x: number) => {
|
|
224
|
+
if (x <= 0.0031308) return x * 12.92
|
|
225
|
+
return 1.055 * Math.pow(x, 1 / 2.4) - 0.055
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
r: Math.round(Math.max(0, Math.min(255, toSrgb(rLinear) * 255))),
|
|
230
|
+
g: Math.round(Math.max(0, Math.min(255, toSrgb(gLinear) * 255))),
|
|
231
|
+
b: Math.round(Math.max(0, Math.min(255, toSrgb(bLinear) * 255))),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// Color Comparison
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
239
|
+
/** Check if two colors are equal */
|
|
240
|
+
export function rgbaEqual(a: RGBA, b: RGBA): boolean {
|
|
241
|
+
return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// Color Blending
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
248
|
+
/** Blend src over dst (standard alpha compositing) */
|
|
249
|
+
export function rgbaBlend(src: RGBA, dst: RGBA): RGBA {
|
|
250
|
+
if (src.a === 255) return src
|
|
251
|
+
if (src.a === 0) return dst
|
|
252
|
+
|
|
253
|
+
const srcA = src.a / 255
|
|
254
|
+
const dstA = dst.a / 255
|
|
255
|
+
const outA = srcA + dstA * (1 - srcA)
|
|
256
|
+
|
|
257
|
+
if (outA === 0) return Colors.TRANSPARENT
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
r: Math.round((src.r * srcA + dst.r * dstA * (1 - srcA)) / outA),
|
|
261
|
+
g: Math.round((src.g * srcA + dst.g * dstA * (1 - srcA)) / outA),
|
|
262
|
+
b: Math.round((src.b * srcA + dst.b * dstA * (1 - srcA)) / outA),
|
|
263
|
+
a: Math.round(outA * 255),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Linear interpolation between two colors */
|
|
268
|
+
export function rgbaLerp(a: RGBA, b: RGBA, t: number): RGBA {
|
|
269
|
+
return {
|
|
270
|
+
r: Math.round(a.r + (b.r - a.r) * t),
|
|
271
|
+
g: Math.round(a.g + (b.g - a.g) * t),
|
|
272
|
+
b: Math.round(a.b + (b.b - a.b) * t),
|
|
273
|
+
a: Math.round(a.a + (b.a - a.a) * t),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// =============================================================================
|
|
278
|
+
// Color Modifiers
|
|
279
|
+
// =============================================================================
|
|
280
|
+
|
|
281
|
+
/** Dim a color (reduce brightness) */
|
|
282
|
+
export function dim(color: RGBA, factor: number = 0.5): RGBA {
|
|
283
|
+
if (isTerminalDefault(color)) {
|
|
284
|
+
return { r: 128, g: 128, b: 128, a: 255 }
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
r: Math.floor(color.r * factor),
|
|
288
|
+
g: Math.floor(color.g * factor),
|
|
289
|
+
b: Math.floor(color.b * factor),
|
|
290
|
+
a: color.a,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Brighten a color (increase brightness) */
|
|
295
|
+
export function brighten(color: RGBA, factor: number = 1.3): RGBA {
|
|
296
|
+
if (isTerminalDefault(color)) {
|
|
297
|
+
return Colors.WHITE
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
r: Math.min(255, Math.floor(color.r * factor)),
|
|
301
|
+
g: Math.min(255, Math.floor(color.g * factor)),
|
|
302
|
+
b: Math.min(255, Math.floor(color.b * factor)),
|
|
303
|
+
a: color.a,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// =============================================================================
|
|
308
|
+
// ANSI Escape Codes
|
|
309
|
+
// =============================================================================
|
|
310
|
+
|
|
311
|
+
/** ANSI escape code for foreground color */
|
|
312
|
+
export function toAnsiFg(color: RGBA): string {
|
|
313
|
+
if (isTerminalDefault(color)) {
|
|
314
|
+
return '\x1b[39m' // Reset to default foreground
|
|
315
|
+
}
|
|
316
|
+
if (isAnsiColor(color)) {
|
|
317
|
+
const index = getAnsiIndex(color)
|
|
318
|
+
// Standard colors 0-7
|
|
319
|
+
if (index >= 0 && index <= 7) {
|
|
320
|
+
return `\x1b[${30 + index}m`
|
|
321
|
+
}
|
|
322
|
+
// Bright colors 8-15
|
|
323
|
+
if (index >= 8 && index <= 15) {
|
|
324
|
+
return `\x1b[${90 + (index - 8)}m`
|
|
325
|
+
}
|
|
326
|
+
// Extended 256-color palette
|
|
327
|
+
return `\x1b[38;5;${index}m`
|
|
328
|
+
}
|
|
329
|
+
return `\x1b[38;2;${color.r};${color.g};${color.b}m`
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** ANSI escape code for background color */
|
|
333
|
+
export function toAnsiBg(color: RGBA): string {
|
|
334
|
+
if (isTerminalDefault(color)) {
|
|
335
|
+
return '\x1b[49m' // Reset to default background
|
|
336
|
+
}
|
|
337
|
+
if (isAnsiColor(color)) {
|
|
338
|
+
const index = getAnsiIndex(color)
|
|
339
|
+
// Standard colors 0-7
|
|
340
|
+
if (index >= 0 && index <= 7) {
|
|
341
|
+
return `\x1b[${40 + index}m`
|
|
342
|
+
}
|
|
343
|
+
// Bright colors 8-15
|
|
344
|
+
if (index >= 8 && index <= 15) {
|
|
345
|
+
return `\x1b[${100 + (index - 8)}m`
|
|
346
|
+
}
|
|
347
|
+
// Extended 256-color palette
|
|
348
|
+
return `\x1b[48;5;${index}m`
|
|
349
|
+
}
|
|
350
|
+
return `\x1b[48;2;${color.r};${color.g};${color.b}m`
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** ANSI escape codes for cell attributes */
|
|
354
|
+
export function toAnsiAttrs(attrs: CellAttrs): string {
|
|
355
|
+
if (attrs === Attr.NONE) return ''
|
|
356
|
+
|
|
357
|
+
const codes: number[] = []
|
|
358
|
+
if (attrs & Attr.BOLD) codes.push(1)
|
|
359
|
+
if (attrs & Attr.DIM) codes.push(2)
|
|
360
|
+
if (attrs & Attr.ITALIC) codes.push(3)
|
|
361
|
+
if (attrs & Attr.UNDERLINE) codes.push(4)
|
|
362
|
+
if (attrs & Attr.BLINK) codes.push(5)
|
|
363
|
+
if (attrs & Attr.INVERSE) codes.push(7)
|
|
364
|
+
if (attrs & Attr.HIDDEN) codes.push(8)
|
|
365
|
+
if (attrs & Attr.STRIKETHROUGH) codes.push(9)
|
|
366
|
+
|
|
367
|
+
return codes.length > 0 ? `\x1b[${codes.join(';')}m` : ''
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Reset all ANSI attributes */
|
|
371
|
+
export const ANSI_RESET = '\x1b[0m'
|
|
372
|
+
|
|
373
|
+
// =============================================================================
|
|
374
|
+
// Character Width (using Bun.stringWidth)
|
|
375
|
+
// =============================================================================
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Get display width of a string in terminal cells.
|
|
379
|
+
* Handles emoji, CJK, combining marks correctly.
|
|
380
|
+
*/
|
|
381
|
+
export function stringWidth(str: string): number {
|
|
382
|
+
return Bun.stringWidth(str)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Get display width of a single character */
|
|
386
|
+
export function charWidth(char: string | number): number {
|
|
387
|
+
const str = typeof char === 'number' ? String.fromCodePoint(char) : char
|
|
388
|
+
return Bun.stringWidth(str)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** ANSI escape code pattern */
|
|
392
|
+
const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g
|
|
393
|
+
|
|
394
|
+
/** Strip ANSI escape codes from a string */
|
|
395
|
+
export function stripAnsi(str: string): string {
|
|
396
|
+
// Use Bun's native if available, otherwise fallback to regex
|
|
397
|
+
if (typeof Bun !== 'undefined' && typeof (Bun as any).stripANSI === 'function') {
|
|
398
|
+
return (Bun as any).stripANSI(str)
|
|
399
|
+
}
|
|
400
|
+
return str.replace(ANSI_PATTERN, '')
|
|
401
|
+
}
|