@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,194 @@
1
+ /**
2
+ * TUI Framework - Layout Engine Types
3
+ *
4
+ * THE terminal layout system. Not just Flexbox - everything.
5
+ *
6
+ * Features:
7
+ * - Full Flexbox (6-phase algorithm with freeze loop)
8
+ * - Grid layout (coming soon)
9
+ * - Fixed/Absolute/Relative positioning
10
+ * - Animation & transitions support (built into the architecture)
11
+ *
12
+ * Pure parallel arrays - NO objects for working data.
13
+ * Every array is indexed by component index.
14
+ */
15
+
16
+ // =============================================================================
17
+ // OUTPUT TYPES (same interface as current Yoga integration)
18
+ // =============================================================================
19
+
20
+ export interface ComputedLayout {
21
+ x: number[]
22
+ y: number[]
23
+ width: number[]
24
+ height: number[]
25
+ scrollable: number[]
26
+ maxScrollY: number[]
27
+ maxScrollX: number[]
28
+ // Content bounds (max extent of all components)
29
+ contentWidth: number // Max x + width across all components
30
+ contentHeight: number // Max y + height across all components
31
+ }
32
+
33
+ // =============================================================================
34
+ // FLEX DIRECTION AND WRAP ENUMS
35
+ // =============================================================================
36
+
37
+ export const FlexDirection = {
38
+ COLUMN: 0,
39
+ ROW: 1,
40
+ COLUMN_REVERSE: 2,
41
+ ROW_REVERSE: 3,
42
+ } as const
43
+
44
+ export const FlexWrap = {
45
+ NO_WRAP: 0,
46
+ WRAP: 1,
47
+ WRAP_REVERSE: 2,
48
+ } as const
49
+
50
+ export const JustifyContent = {
51
+ FLEX_START: 0,
52
+ CENTER: 1,
53
+ FLEX_END: 2,
54
+ SPACE_BETWEEN: 3,
55
+ SPACE_AROUND: 4,
56
+ SPACE_EVENLY: 5,
57
+ } as const
58
+
59
+ export const AlignItems = {
60
+ STRETCH: 0,
61
+ FLEX_START: 1,
62
+ CENTER: 2,
63
+ FLEX_END: 3,
64
+ BASELINE: 4,
65
+ } as const
66
+
67
+ // =============================================================================
68
+ // POSITIONING
69
+ // =============================================================================
70
+
71
+ export const Position = {
72
+ RELATIVE: 0, // Normal flow, offsets relative to normal position
73
+ ABSOLUTE: 1, // Out of flow, positioned relative to nearest positioned ancestor
74
+ FIXED: 2, // Out of flow, positioned relative to terminal viewport
75
+ STICKY: 3, // Hybrid - relative until scroll threshold, then fixed
76
+ } as const
77
+
78
+ // =============================================================================
79
+ // DISPLAY MODE (for future Grid support)
80
+ // =============================================================================
81
+
82
+ export const Display = {
83
+ FLEX: 0, // Flexbox layout
84
+ GRID: 1, // Grid layout (future)
85
+ NONE: 2, // Hidden, not in layout
86
+ } as const
87
+
88
+ // =============================================================================
89
+ // OVERFLOW (for scroll containers)
90
+ // =============================================================================
91
+
92
+ export const Overflow = {
93
+ VISIBLE: 0, // Content can overflow
94
+ HIDDEN: 1, // Content clipped
95
+ SCROLL: 2, // Scrollable
96
+ AUTO: 3, // Scroll only if needed
97
+ } as const
98
+
99
+ // =============================================================================
100
+ // WORKING DATA - Pure Parallel Arrays
101
+ // =============================================================================
102
+
103
+ /**
104
+ * All working data for layout computation.
105
+ * Every array is indexed by component index.
106
+ * Pre-allocated for performance, reset each layout pass.
107
+ */
108
+ export interface WorkingData {
109
+ // Hierarchy (computed once per layout)
110
+ depth: number[] // Nesting depth (root = 0)
111
+ childStart: number[] // Index into childrenOrdered
112
+ childCount: number[] // Number of direct children
113
+
114
+ // Flex working data
115
+ flexBaseSize: number[] // Initial size before flex
116
+ hypotheticalMainSize: number[] // Clamped by min/max
117
+ targetMainSize: number[] // Size after flex distribution
118
+ targetCrossSize: number[] // Cross axis size
119
+ frozen: Uint8Array // 0 = unfrozen, 1 = frozen
120
+
121
+ // Final computed values
122
+ computedX: number[]
123
+ computedY: number[]
124
+ computedWidth: number[]
125
+ computedHeight: number[]
126
+
127
+ // Scroll detection
128
+ contentWidth: number[] // Total content width
129
+ contentHeight: number[] // Total content height
130
+ }
131
+
132
+ /**
133
+ * Line data for flex wrap.
134
+ * Stored separately since number of lines varies per container.
135
+ */
136
+ export interface LineData {
137
+ // Per-container line info
138
+ lineStart: number[] // Index into items array where lines begin
139
+ lineCount: number[] // Number of lines in this container
140
+
141
+ // Flattened line items (all lines concatenated)
142
+ items: number[][] // items[lineIndex] = array of component indices
143
+
144
+ // Per-line computed data
145
+ lineCrossSize: number[] // Cross size of each line
146
+ }
147
+
148
+ // =============================================================================
149
+ // HELPER TYPES
150
+ // =============================================================================
151
+
152
+ export type FlexDirectionValue = typeof FlexDirection[keyof typeof FlexDirection]
153
+ export type FlexWrapValue = typeof FlexWrap[keyof typeof FlexWrap]
154
+ export type JustifyContentValue = typeof JustifyContent[keyof typeof JustifyContent]
155
+ export type AlignItemsValue = typeof AlignItems[keyof typeof AlignItems]
156
+ export type PositionValue = typeof Position[keyof typeof Position]
157
+ export type DisplayValue = typeof Display[keyof typeof Display]
158
+ export type OverflowValue = typeof Overflow[keyof typeof Overflow]
159
+
160
+ // =============================================================================
161
+ // ANIMATION TYPES (built into architecture from day 1)
162
+ // =============================================================================
163
+
164
+ export interface TransitionConfig {
165
+ property: string // Which property to animate
166
+ duration: number // Duration in ms
167
+ easing: EasingFunction // Easing function
168
+ delay: number // Delay before start
169
+ }
170
+
171
+ export type EasingFunction =
172
+ | 'linear'
173
+ | 'ease'
174
+ | 'ease-in'
175
+ | 'ease-out'
176
+ | 'ease-in-out'
177
+ | 'step-start'
178
+ | 'step-end'
179
+ | ((t: number) => number) // Custom easing function
180
+
181
+ // Animation state per component (for future use)
182
+ export interface AnimationState {
183
+ startValue: number
184
+ endValue: number
185
+ startTime: number
186
+ duration: number
187
+ easing: EasingFunction
188
+ }
189
+
190
+ // =============================================================================
191
+ // CONSTANTS
192
+ // =============================================================================
193
+
194
+ export const INFINITY = 999999 // Terminal has finite size, use large number
@@ -0,0 +1,202 @@
1
+ /**
2
+ * TUI Framework - Hierarchy Utilities
3
+ *
4
+ * Builds hierarchy data from parallel arrays for efficient iteration.
5
+ * NO recursion - just flat loops over arrays.
6
+ *
7
+ * Key insight: Instead of walking a tree, we:
8
+ * 1. Calculate depth for each component (one loop)
9
+ * 2. Group children by parent (one loop)
10
+ * 3. Build ordered children array (one loop)
11
+ *
12
+ * Result: O(n) regardless of nesting depth.
13
+ */
14
+
15
+ import { unwrap } from '@rlabs-inc/signals'
16
+ import * as core from '../../../engine/arrays/core'
17
+ import * as layout from '../../../engine/arrays/layout'
18
+
19
+ // =============================================================================
20
+ // HIERARCHY DATA (reused across layouts)
21
+ // =============================================================================
22
+
23
+ // Depth of each component (root = 0)
24
+ export const depth: number[] = []
25
+
26
+ // Children organization
27
+ export const childStart: number[] = [] // Index into childrenOrdered
28
+ export const childCount: number[] = [] // Number of direct children
29
+
30
+ // Flat array of all children, ordered by parent
31
+ export const childrenOrdered: number[] = []
32
+
33
+ // Root components (parentIndex === -1)
34
+ export const roots: number[] = []
35
+
36
+ // Components sorted by depth (for bottom-up/top-down iteration)
37
+ export const sortedByDepth: number[] = []
38
+
39
+ // Maximum depth in the tree
40
+ export let maxDepth = 0
41
+
42
+ // =============================================================================
43
+ // HIERARCHY PREPARATION
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Prepare hierarchy data for layout.
48
+ * Called once at the start of each layout pass.
49
+ *
50
+ * This replaces recursive tree traversal with flat array iteration.
51
+ */
52
+ export function prepareHierarchy(indices: Set<number>): void {
53
+ // Reset arrays
54
+ roots.length = 0
55
+ childrenOrdered.length = 0
56
+ sortedByDepth.length = 0
57
+ maxDepth = 0
58
+
59
+ if (indices.size === 0) return
60
+
61
+ // Step 1: Calculate depth for each component
62
+ // This is the only "recursive-like" part, but we do it iteratively
63
+ for (const i of indices) {
64
+ let d = 0
65
+ let p = unwrap(core.parentIndex[i]) ?? -1
66
+
67
+ // Walk up to root, counting depth
68
+ while (p >= 0 && indices.has(p)) {
69
+ d++
70
+ p = unwrap(core.parentIndex[p]) ?? -1
71
+ }
72
+
73
+ depth[i] = d
74
+ if (d > maxDepth) maxDepth = d
75
+
76
+ // Track roots
77
+ if (d === 0) {
78
+ roots.push(i)
79
+ }
80
+ }
81
+
82
+ // Step 2: Group children by parent
83
+ const childrenByParent = new Map<number, number[]>()
84
+
85
+ for (const i of indices) {
86
+ const parent = unwrap(core.parentIndex[i]) ?? -1
87
+
88
+ if (parent >= 0) {
89
+ let children = childrenByParent.get(parent)
90
+ if (!children) {
91
+ children = []
92
+ childrenByParent.set(parent, children)
93
+ }
94
+ children.push(i)
95
+ }
96
+ }
97
+
98
+ // Step 3: Sort children within each parent by order property
99
+ for (const children of childrenByParent.values()) {
100
+ children.sort((a, b) => {
101
+ const orderA = unwrap(layout.order[a]) ?? 0
102
+ const orderB = unwrap(layout.order[b]) ?? 0
103
+ return orderA - orderB
104
+ })
105
+ }
106
+
107
+ // Step 4: Build childrenOrdered array and set childStart/childCount
108
+ let offset = 0
109
+
110
+ for (const i of indices) {
111
+ const children = childrenByParent.get(i)
112
+
113
+ if (children && children.length > 0) {
114
+ childStart[i] = offset
115
+ childCount[i] = children.length
116
+
117
+ for (const child of children) {
118
+ childrenOrdered[offset++] = child
119
+ }
120
+ } else {
121
+ childStart[i] = 0
122
+ childCount[i] = 0
123
+ }
124
+ }
125
+
126
+ // Step 5: Sort all indices by depth
127
+ for (const i of indices) {
128
+ sortedByDepth.push(i)
129
+ }
130
+ sortedByDepth.sort((a, b) => depth[a]! - depth[b]!)
131
+ }
132
+
133
+ /**
134
+ * Get children of a component.
135
+ * Returns array slice from childrenOrdered.
136
+ */
137
+ export function getChildren(index: number): number[] {
138
+ const start = childStart[index] ?? 0
139
+ const count = childCount[index] ?? 0
140
+
141
+ if (count === 0) return []
142
+
143
+ const result: number[] = []
144
+ for (let i = 0; i < count; i++) {
145
+ result.push(childrenOrdered[start + i]!)
146
+ }
147
+ return result
148
+ }
149
+
150
+ /**
151
+ * Check if component has children.
152
+ */
153
+ export function hasChildren(index: number): boolean {
154
+ return (childCount[index] ?? 0) > 0
155
+ }
156
+
157
+ /**
158
+ * Get depth of a component.
159
+ */
160
+ export function getDepth(index: number): number {
161
+ return depth[index] ?? 0
162
+ }
163
+
164
+ /**
165
+ * Iterate components in depth order (roots first, then children).
166
+ * Useful for top-down traversal.
167
+ */
168
+ export function* iterateTopDown(): Generator<number> {
169
+ for (const i of sortedByDepth) {
170
+ yield i
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Iterate components in reverse depth order (deepest first).
176
+ * Useful for bottom-up traversal (content sizing).
177
+ */
178
+ export function* iterateBottomUp(): Generator<number> {
179
+ for (let i = sortedByDepth.length - 1; i >= 0; i--) {
180
+ yield sortedByDepth[i]!
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get the root components.
186
+ */
187
+ export function getRoots(): number[] {
188
+ return roots
189
+ }
190
+
191
+ /**
192
+ * Reset hierarchy data (called when clearing layout).
193
+ */
194
+ export function resetHierarchy(): void {
195
+ depth.length = 0
196
+ childStart.length = 0
197
+ childCount.length = 0
198
+ childrenOrdered.length = 0
199
+ roots.length = 0
200
+ sortedByDepth.length = 0
201
+ maxDepth = 0
202
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * TUI Framework - Layout Math Utilities
3
+ *
4
+ * Integer-only math for terminal cells.
5
+ * No sub-pixel nonsense - terminals work in discrete cells.
6
+ */
7
+
8
+ /**
9
+ * Clamp a value between min and max.
10
+ * Returns integer.
11
+ */
12
+ export function clamp(min: number, value: number, max: number): number {
13
+ if (value < min) return min
14
+ if (value > max) return max
15
+ return value
16
+ }
17
+
18
+ /**
19
+ * Clamp and round to integer.
20
+ * Use at final step when writing to computed arrays.
21
+ */
22
+ export function clampInt(min: number, value: number, max: number): number {
23
+ return Math.round(clamp(min, value, max))
24
+ }
25
+
26
+ /**
27
+ * Round to nearest integer.
28
+ * Always round at the end, not during intermediate calculations.
29
+ */
30
+ export function round(value: number): number {
31
+ return Math.round(value)
32
+ }
33
+
34
+ /**
35
+ * Floor to integer.
36
+ * Use for flex grow to avoid overflow.
37
+ */
38
+ export function floor(value: number): number {
39
+ return Math.floor(value)
40
+ }
41
+
42
+ /**
43
+ * Ceiling to integer.
44
+ * Use for flex shrink to ensure minimum coverage.
45
+ */
46
+ export function ceil(value: number): number {
47
+ return Math.ceil(value)
48
+ }
49
+
50
+ /**
51
+ * Check if a number is effectively zero (within epsilon).
52
+ */
53
+ export function isZero(value: number, epsilon = 0.001): boolean {
54
+ return Math.abs(value) < epsilon
55
+ }
56
+
57
+ /**
58
+ * Distribute a total amount across N items proportionally.
59
+ * Returns array of integer amounts that sum to exactly `total`.
60
+ *
61
+ * Uses largest remainder method to ensure exact distribution.
62
+ */
63
+ export function distributeProportionally(
64
+ total: number,
65
+ weights: number[]
66
+ ): number[] {
67
+ if (weights.length === 0) return []
68
+
69
+ const totalWeight = weights.reduce((a, b) => a + b, 0)
70
+ if (totalWeight === 0) {
71
+ // Equal distribution if no weights
72
+ const base = Math.floor(total / weights.length)
73
+ const remainder = total - base * weights.length
74
+ return weights.map((_, i) => base + (i < remainder ? 1 : 0))
75
+ }
76
+
77
+ // Calculate exact (floating) shares
78
+ const exact = weights.map(w => (total * w) / totalWeight)
79
+
80
+ // Floor each share
81
+ const floored = exact.map(Math.floor)
82
+
83
+ // Calculate remainders
84
+ const remainders = exact.map((e, i) => ({
85
+ index: i,
86
+ remainder: e - floored[i]!,
87
+ }))
88
+
89
+ // Sort by remainder descending
90
+ remainders.sort((a, b) => b.remainder - a.remainder)
91
+
92
+ // Distribute leftover to items with largest remainders
93
+ const distributed = total - floored.reduce((a, b) => a + b, 0)
94
+ for (let i = 0; i < distributed; i++) {
95
+ floored[remainders[i]!.index]!++
96
+ }
97
+
98
+ return floored
99
+ }
100
+
101
+ /**
102
+ * Sum an array of numbers.
103
+ */
104
+ export function sum(arr: number[]): number {
105
+ let total = 0
106
+ for (let i = 0; i < arr.length; i++) {
107
+ total += arr[i]!
108
+ }
109
+ return total
110
+ }
111
+
112
+ /**
113
+ * Find maximum in an array.
114
+ */
115
+ export function max(arr: number[]): number {
116
+ if (arr.length === 0) return 0
117
+ let m = arr[0]!
118
+ for (let i = 1; i < arr.length; i++) {
119
+ if (arr[i]! > m) m = arr[i]!
120
+ }
121
+ return m
122
+ }
123
+
124
+ /**
125
+ * Find minimum in an array.
126
+ */
127
+ export function min(arr: number[]): number {
128
+ if (arr.length === 0) return 0
129
+ let m = arr[0]!
130
+ for (let i = 1; i < arr.length; i++) {
131
+ if (arr[i]! < m) m = arr[i]!
132
+ }
133
+ return m
134
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * TUI Framework - Text Measurement
3
+ *
4
+ * Terminal text measurement using Bun.stringWidth.
5
+ * Handles Unicode, emoji, CJK wide characters correctly.
6
+ */
7
+
8
+ import { stringWidth } from '../../../utils/text'
9
+
10
+ /**
11
+ * Measure the width of a string in terminal cells.
12
+ * Handles:
13
+ * - Regular ASCII (1 cell each)
14
+ * - Emoji (usually 2 cells)
15
+ * - CJK characters (2 cells)
16
+ * - Control characters (0 cells)
17
+ */
18
+ export function measureTextWidth(content: string): number {
19
+ return stringWidth(content)
20
+ }
21
+
22
+ /**
23
+ * Measure text height when wrapped to a maximum width.
24
+ * Returns number of lines.
25
+ */
26
+ export function measureTextHeight(content: string, maxWidth: number): number {
27
+ if (!content || maxWidth <= 0) return 0
28
+
29
+ // Simple word wrap - count lines
30
+ let lines = 1
31
+ let currentLineWidth = 0
32
+
33
+ // Split by existing newlines first
34
+ const paragraphs = content.split('\n')
35
+
36
+ for (const paragraph of paragraphs) {
37
+ if (paragraph === '') {
38
+ lines++
39
+ continue
40
+ }
41
+
42
+ // Process each character
43
+ for (const char of paragraph) {
44
+ const charWidth = stringWidth(char)
45
+
46
+ if (currentLineWidth + charWidth > maxWidth && currentLineWidth > 0) {
47
+ lines++
48
+ currentLineWidth = charWidth
49
+ } else {
50
+ currentLineWidth += charWidth
51
+ }
52
+ }
53
+
54
+ // Reset for next paragraph
55
+ currentLineWidth = 0
56
+ if (paragraphs.indexOf(paragraph) < paragraphs.length - 1) {
57
+ lines++
58
+ }
59
+ }
60
+
61
+ return lines
62
+ }
63
+
64
+ /**
65
+ * Measure text dimensions for layout.
66
+ * Returns both width (longest line) and height (number of lines).
67
+ */
68
+ export function measureText(
69
+ content: string,
70
+ maxWidth: number
71
+ ): { width: number; height: number } {
72
+ if (!content) return { width: 0, height: 0 }
73
+
74
+ const lines = wrapTextLines(content, maxWidth)
75
+ let maxLineWidth = 0
76
+
77
+ for (const line of lines) {
78
+ const lineWidth = stringWidth(line)
79
+ if (lineWidth > maxLineWidth) {
80
+ maxLineWidth = lineWidth
81
+ }
82
+ }
83
+
84
+ return {
85
+ width: maxLineWidth,
86
+ height: lines.length,
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Wrap text to a maximum width, returning array of lines.
92
+ * Simple word-boundary wrapping with fallback to character wrap.
93
+ */
94
+ export function wrapTextLines(content: string, maxWidth: number): string[] {
95
+ if (!content) return []
96
+ if (maxWidth <= 0) return [content]
97
+
98
+ const result: string[] = []
99
+ const paragraphs = content.split('\n')
100
+
101
+ for (const paragraph of paragraphs) {
102
+ if (paragraph === '') {
103
+ result.push('')
104
+ continue
105
+ }
106
+
107
+ let currentLine = ''
108
+ let currentWidth = 0
109
+ const words = paragraph.split(' ')
110
+
111
+ for (let i = 0; i < words.length; i++) {
112
+ const word = words[i]!
113
+ const wordWidth = stringWidth(word)
114
+
115
+ // Check if word fits on current line
116
+ const needsSpace = currentLine.length > 0
117
+ const spaceWidth = needsSpace ? 1 : 0
118
+
119
+ if (currentWidth + spaceWidth + wordWidth <= maxWidth) {
120
+ // Word fits
121
+ if (needsSpace) {
122
+ currentLine += ' '
123
+ currentWidth += 1
124
+ }
125
+ currentLine += word
126
+ currentWidth += wordWidth
127
+ } else if (currentLine.length === 0) {
128
+ // Word is too long for any line - character wrap
129
+ let remaining = word
130
+ while (remaining.length > 0) {
131
+ let chunk = ''
132
+ let chunkWidth = 0
133
+ for (const char of remaining) {
134
+ const charWidth = stringWidth(char)
135
+ if (chunkWidth + charWidth > maxWidth && chunk.length > 0) {
136
+ break
137
+ }
138
+ chunk += char
139
+ chunkWidth += charWidth
140
+ }
141
+ result.push(chunk)
142
+ remaining = remaining.slice(chunk.length)
143
+ }
144
+ currentLine = ''
145
+ currentWidth = 0
146
+ } else {
147
+ // Start new line
148
+ result.push(currentLine)
149
+ currentLine = word
150
+ currentWidth = wordWidth
151
+ }
152
+ }
153
+
154
+ if (currentLine.length > 0) {
155
+ result.push(currentLine)
156
+ }
157
+ }
158
+
159
+ return result.length > 0 ? result : ['']
160
+ }