@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.
Files changed (44) hide show
  1. package/README.md +141 -0
  2. package/index.ts +45 -0
  3. package/package.json +59 -0
  4. package/src/api/index.ts +7 -0
  5. package/src/api/mount.ts +230 -0
  6. package/src/engine/arrays/core.ts +60 -0
  7. package/src/engine/arrays/dimensions.ts +68 -0
  8. package/src/engine/arrays/index.ts +166 -0
  9. package/src/engine/arrays/interaction.ts +112 -0
  10. package/src/engine/arrays/layout.ts +175 -0
  11. package/src/engine/arrays/spacing.ts +100 -0
  12. package/src/engine/arrays/text.ts +55 -0
  13. package/src/engine/arrays/visual.ts +140 -0
  14. package/src/engine/index.ts +25 -0
  15. package/src/engine/inheritance.ts +138 -0
  16. package/src/engine/registry.ts +180 -0
  17. package/src/pipeline/frameBuffer.ts +473 -0
  18. package/src/pipeline/layout/index.ts +105 -0
  19. package/src/pipeline/layout/titan-engine.ts +798 -0
  20. package/src/pipeline/layout/types.ts +194 -0
  21. package/src/pipeline/layout/utils/hierarchy.ts +202 -0
  22. package/src/pipeline/layout/utils/math.ts +134 -0
  23. package/src/pipeline/layout/utils/text-measure.ts +160 -0
  24. package/src/pipeline/layout.ts +30 -0
  25. package/src/primitives/box.ts +312 -0
  26. package/src/primitives/index.ts +12 -0
  27. package/src/primitives/text.ts +199 -0
  28. package/src/primitives/types.ts +222 -0
  29. package/src/primitives/utils.ts +37 -0
  30. package/src/renderer/ansi.ts +625 -0
  31. package/src/renderer/buffer.ts +667 -0
  32. package/src/renderer/index.ts +40 -0
  33. package/src/renderer/input.ts +518 -0
  34. package/src/renderer/output.ts +451 -0
  35. package/src/state/cursor.ts +176 -0
  36. package/src/state/focus.ts +241 -0
  37. package/src/state/index.ts +43 -0
  38. package/src/state/keyboard.ts +771 -0
  39. package/src/state/mouse.ts +524 -0
  40. package/src/state/scroll.ts +341 -0
  41. package/src/state/theme.ts +687 -0
  42. package/src/types/color.ts +401 -0
  43. package/src/types/index.ts +316 -0
  44. 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
+ }