@effect-tui/core 0.1.1 → 0.1.4

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 (271) hide show
  1. package/README.md +31 -11
  2. package/dist/ansi.d.ts +127 -32
  3. package/dist/ansi.d.ts.map +1 -1
  4. package/dist/ansi.js +159 -37
  5. package/dist/ansi.js.map +1 -1
  6. package/dist/colors.d.ts +139 -0
  7. package/dist/colors.d.ts.map +1 -0
  8. package/dist/colors.js +339 -0
  9. package/dist/colors.js.map +1 -0
  10. package/dist/index.d.ts +6 -10
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -11
  13. package/dist/index.js.map +1 -1
  14. package/dist/keys.d.ts +21 -0
  15. package/dist/keys.d.ts.map +1 -1
  16. package/dist/keys.js +199 -58
  17. package/dist/keys.js.map +1 -1
  18. package/dist/layout/axis-helpers.d.ts +19 -0
  19. package/dist/layout/axis-helpers.d.ts.map +1 -0
  20. package/dist/layout/axis-helpers.js +19 -0
  21. package/dist/layout/axis-helpers.js.map +1 -0
  22. package/dist/output.d.ts +59 -0
  23. package/dist/output.d.ts.map +1 -0
  24. package/dist/output.js +142 -0
  25. package/dist/output.js.map +1 -0
  26. package/dist/render/buffer.d.ts.map +1 -1
  27. package/dist/render/buffer.js +6 -25
  28. package/dist/render/buffer.js.map +1 -1
  29. package/dist/render/graphemes.d.ts +15 -0
  30. package/dist/render/graphemes.d.ts.map +1 -0
  31. package/dist/render/graphemes.js +28 -0
  32. package/dist/render/graphemes.js.map +1 -0
  33. package/dist/render/measure.d.ts +1 -0
  34. package/dist/render/measure.d.ts.map +1 -1
  35. package/dist/render/measure.js +14 -36
  36. package/dist/render/measure.js.map +1 -1
  37. package/dist/render/palette.d.ts.map +1 -1
  38. package/dist/render/palette.js +26 -1
  39. package/dist/render/palette.js.map +1 -1
  40. package/dist/render/segmenter.d.ts +8 -0
  41. package/dist/render/segmenter.d.ts.map +1 -0
  42. package/dist/render/segmenter.js +23 -0
  43. package/dist/render/segmenter.js.map +1 -0
  44. package/dist/render/surface.d.ts +6 -32
  45. package/dist/render/surface.d.ts.map +1 -1
  46. package/dist/render/surface.js +11 -80
  47. package/dist/render/surface.js.map +1 -1
  48. package/dist/runtime/backend_node.d.ts.map +1 -1
  49. package/dist/runtime/backend_node.js.map +1 -1
  50. package/dist/tailwind-colors.d.ts +291 -0
  51. package/dist/tailwind-colors.d.ts.map +1 -0
  52. package/dist/tailwind-colors.js +291 -0
  53. package/dist/tailwind-colors.js.map +1 -0
  54. package/dist/types.d.ts +15 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +3 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +55 -55
  59. package/src/ansi.ts +201 -73
  60. package/src/colors.ts +468 -0
  61. package/src/index.ts +28 -14
  62. package/src/keys.ts +467 -287
  63. package/src/layout/axis-helpers.ts +33 -0
  64. package/src/output.ts +175 -0
  65. package/src/render/buffer.ts +161 -184
  66. package/src/render/graphemes.ts +34 -0
  67. package/src/render/measure.ts +15 -38
  68. package/src/render/palette.ts +98 -77
  69. package/src/render/segmenter.ts +27 -0
  70. package/src/render/surface.ts +139 -225
  71. package/src/runtime/backend_node.ts +71 -71
  72. package/src/tailwind-colors.ts +295 -0
  73. package/src/types.ts +18 -0
  74. package/dist/anim.d.ts +0 -4
  75. package/dist/anim.d.ts.map +0 -1
  76. package/dist/anim.js +0 -5
  77. package/dist/anim.js.map +0 -1
  78. package/dist/layout/linearStack.d.ts +0 -17
  79. package/dist/layout/linearStack.d.ts.map +0 -1
  80. package/dist/layout/linearStack.js +0 -86
  81. package/dist/layout/linearStack.js.map +0 -1
  82. package/dist/motion-value.d.ts +0 -58
  83. package/dist/motion-value.d.ts.map +0 -1
  84. package/dist/motion-value.js +0 -250
  85. package/dist/motion-value.js.map +0 -1
  86. package/dist/present/display.d.ts +0 -58
  87. package/dist/present/display.d.ts.map +0 -1
  88. package/dist/present/display.js +0 -168
  89. package/dist/present/display.js.map +0 -1
  90. package/dist/present/writers/fullscreen.d.ts +0 -19
  91. package/dist/present/writers/fullscreen.d.ts.map +0 -1
  92. package/dist/present/writers/fullscreen.js +0 -55
  93. package/dist/present/writers/fullscreen.js.map +0 -1
  94. package/dist/present/writers/inline.d.ts +0 -20
  95. package/dist/present/writers/inline.d.ts.map +0 -1
  96. package/dist/present/writers/inline.js +0 -92
  97. package/dist/present/writers/inline.js.map +0 -1
  98. package/dist/render/color-utils.d.ts +0 -18
  99. package/dist/render/color-utils.d.ts.map +0 -1
  100. package/dist/render/color-utils.js +0 -58
  101. package/dist/render/color-utils.js.map +0 -1
  102. package/dist/render/diff.d.ts +0 -30
  103. package/dist/render/diff.d.ts.map +0 -1
  104. package/dist/render/diff.js +0 -83
  105. package/dist/render/diff.js.map +0 -1
  106. package/dist/spring-physics.d.ts +0 -36
  107. package/dist/spring-physics.d.ts.map +0 -1
  108. package/dist/spring-physics.js +0 -113
  109. package/dist/spring-physics.js.map +0 -1
  110. package/dist/spring.d.ts +0 -73
  111. package/dist/spring.d.ts.map +0 -1
  112. package/dist/spring.js +0 -136
  113. package/dist/spring.js.map +0 -1
  114. package/dist/ui/containers/canvas.d.ts +0 -13
  115. package/dist/ui/containers/canvas.d.ts.map +0 -1
  116. package/dist/ui/containers/canvas.js +0 -16
  117. package/dist/ui/containers/canvas.js.map +0 -1
  118. package/dist/ui/containers/geometry-reader.d.ts +0 -17
  119. package/dist/ui/containers/geometry-reader.d.ts.map +0 -1
  120. package/dist/ui/containers/geometry-reader.js +0 -24
  121. package/dist/ui/containers/geometry-reader.js.map +0 -1
  122. package/dist/ui/containers/hstack.d.ts +0 -12
  123. package/dist/ui/containers/hstack.d.ts.map +0 -1
  124. package/dist/ui/containers/hstack.js +0 -28
  125. package/dist/ui/containers/hstack.js.map +0 -1
  126. package/dist/ui/containers/scroll.d.ts +0 -28
  127. package/dist/ui/containers/scroll.d.ts.map +0 -1
  128. package/dist/ui/containers/scroll.js +0 -97
  129. package/dist/ui/containers/scroll.js.map +0 -1
  130. package/dist/ui/containers/shared.d.ts +0 -12
  131. package/dist/ui/containers/shared.d.ts.map +0 -1
  132. package/dist/ui/containers/shared.js +0 -19
  133. package/dist/ui/containers/shared.js.map +0 -1
  134. package/dist/ui/containers/vstack.d.ts +0 -12
  135. package/dist/ui/containers/vstack.d.ts.map +0 -1
  136. package/dist/ui/containers/vstack.js +0 -28
  137. package/dist/ui/containers/vstack.js.map +0 -1
  138. package/dist/ui/containers/zstack.d.ts +0 -14
  139. package/dist/ui/containers/zstack.d.ts.map +0 -1
  140. package/dist/ui/containers/zstack.js +0 -36
  141. package/dist/ui/containers/zstack.js.map +0 -1
  142. package/dist/ui/core/geometry-store.d.ts +0 -22
  143. package/dist/ui/core/geometry-store.d.ts.map +0 -1
  144. package/dist/ui/core/geometry-store.js +0 -29
  145. package/dist/ui/core/geometry-store.js.map +0 -1
  146. package/dist/ui/core/geometry.d.ts +0 -34
  147. package/dist/ui/core/geometry.d.ts.map +0 -1
  148. package/dist/ui/core/geometry.js +0 -14
  149. package/dist/ui/core/geometry.js.map +0 -1
  150. package/dist/ui/core/view.d.ts +0 -25
  151. package/dist/ui/core/view.d.ts.map +0 -1
  152. package/dist/ui/core/view.js +0 -34
  153. package/dist/ui/core/view.js.map +0 -1
  154. package/dist/ui/index.d.ts +0 -44
  155. package/dist/ui/index.d.ts.map +0 -1
  156. package/dist/ui/index.js +0 -39
  157. package/dist/ui/index.js.map +0 -1
  158. package/dist/ui/inlinetext.d.ts +0 -24
  159. package/dist/ui/inlinetext.d.ts.map +0 -1
  160. package/dist/ui/inlinetext.js +0 -131
  161. package/dist/ui/inlinetext.js.map +0 -1
  162. package/dist/ui/install.d.ts +0 -22
  163. package/dist/ui/install.d.ts.map +0 -1
  164. package/dist/ui/install.js +0 -66
  165. package/dist/ui/install.js.map +0 -1
  166. package/dist/ui/markdown.d.ts +0 -40
  167. package/dist/ui/markdown.d.ts.map +0 -1
  168. package/dist/ui/markdown.js +0 -351
  169. package/dist/ui/markdown.js.map +0 -1
  170. package/dist/ui/modifiers/border.d.ts +0 -33
  171. package/dist/ui/modifiers/border.d.ts.map +0 -1
  172. package/dist/ui/modifiers/border.js +0 -82
  173. package/dist/ui/modifiers/border.js.map +0 -1
  174. package/dist/ui/modifiers/fill.d.ts +0 -14
  175. package/dist/ui/modifiers/fill.d.ts.map +0 -1
  176. package/dist/ui/modifiers/fill.js +0 -25
  177. package/dist/ui/modifiers/fill.js.map +0 -1
  178. package/dist/ui/modifiers/frame.d.ts +0 -23
  179. package/dist/ui/modifiers/frame.d.ts.map +0 -1
  180. package/dist/ui/modifiers/frame.js +0 -54
  181. package/dist/ui/modifiers/frame.js.map +0 -1
  182. package/dist/ui/modifiers/offset.d.ts +0 -15
  183. package/dist/ui/modifiers/offset.d.ts.map +0 -1
  184. package/dist/ui/modifiers/offset.js +0 -21
  185. package/dist/ui/modifiers/offset.js.map +0 -1
  186. package/dist/ui/modifiers/opacity.d.ts +0 -15
  187. package/dist/ui/modifiers/opacity.d.ts.map +0 -1
  188. package/dist/ui/modifiers/opacity.js +0 -95
  189. package/dist/ui/modifiers/opacity.js.map +0 -1
  190. package/dist/ui/modifiers/padding.d.ts +0 -20
  191. package/dist/ui/modifiers/padding.d.ts.map +0 -1
  192. package/dist/ui/modifiers/padding.js +0 -36
  193. package/dist/ui/modifiers/padding.js.map +0 -1
  194. package/dist/ui/modifiers/styled.d.ts +0 -14
  195. package/dist/ui/modifiers/styled.d.ts.map +0 -1
  196. package/dist/ui/modifiers/styled.js +0 -26
  197. package/dist/ui/modifiers/styled.js.map +0 -1
  198. package/dist/ui/primitives/rectangle.d.ts +0 -15
  199. package/dist/ui/primitives/rectangle.d.ts.map +0 -1
  200. package/dist/ui/primitives/rectangle.js +0 -23
  201. package/dist/ui/primitives/rectangle.js.map +0 -1
  202. package/dist/ui/primitives/spacer.d.ts +0 -13
  203. package/dist/ui/primitives/spacer.d.ts.map +0 -1
  204. package/dist/ui/primitives/spacer.js +0 -16
  205. package/dist/ui/primitives/spacer.js.map +0 -1
  206. package/dist/ui/primitives/text.d.ts +0 -15
  207. package/dist/ui/primitives/text.d.ts.map +0 -1
  208. package/dist/ui/primitives/text.js +0 -79
  209. package/dist/ui/primitives/text.js.map +0 -1
  210. package/dist/ui/primitives/wrapped-text.d.ts +0 -30
  211. package/dist/ui/primitives/wrapped-text.d.ts.map +0 -1
  212. package/dist/ui/primitives/wrapped-text.js +0 -117
  213. package/dist/ui/primitives/wrapped-text.js.map +0 -1
  214. package/dist/ui/shinytext.d.ts +0 -66
  215. package/dist/ui/shinytext.d.ts.map +0 -1
  216. package/dist/ui/shinytext.js +0 -99
  217. package/dist/ui/shinytext.js.map +0 -1
  218. package/dist/ui/text/layout.d.ts +0 -35
  219. package/dist/ui/text/layout.d.ts.map +0 -1
  220. package/dist/ui/text/layout.js +0 -102
  221. package/dist/ui/text/layout.js.map +0 -1
  222. package/dist/ui/textinput.d.ts +0 -140
  223. package/dist/ui/textinput.d.ts.map +0 -1
  224. package/dist/ui/textinput.js +0 -402
  225. package/dist/ui/textinput.js.map +0 -1
  226. package/dist/ui/view-constructors.d.ts +0 -72
  227. package/dist/ui/view-constructors.d.ts.map +0 -1
  228. package/dist/ui/view-constructors.js +0 -74
  229. package/dist/ui/view-constructors.js.map +0 -1
  230. package/src/anim.ts +0 -5
  231. package/src/layout/linearStack.ts +0 -115
  232. package/src/motion-value.ts +0 -335
  233. package/src/present/display.ts +0 -206
  234. package/src/present/writers/fullscreen.ts +0 -58
  235. package/src/present/writers/inline.ts +0 -101
  236. package/src/render/color-utils.ts +0 -60
  237. package/src/render/diff.ts +0 -95
  238. package/src/spring-physics.ts +0 -151
  239. package/src/spring.ts +0 -234
  240. package/src/ui/__snapshots__/wrappedtext.test.ts.snap +0 -57
  241. package/src/ui/containers/canvas.ts +0 -18
  242. package/src/ui/containers/geometry-reader.ts +0 -32
  243. package/src/ui/containers/hstack.ts +0 -33
  244. package/src/ui/containers/scroll.ts +0 -106
  245. package/src/ui/containers/shared.ts +0 -27
  246. package/src/ui/containers/vstack.ts +0 -34
  247. package/src/ui/containers/zstack.ts +0 -37
  248. package/src/ui/core/geometry-store.ts +0 -42
  249. package/src/ui/core/geometry.ts +0 -30
  250. package/src/ui/core/view.ts +0 -49
  251. package/src/ui/index.ts +0 -84
  252. package/src/ui/inlinetext.ts +0 -135
  253. package/src/ui/install.ts +0 -110
  254. package/src/ui/markdown.test.ts +0 -74
  255. package/src/ui/markdown.ts +0 -388
  256. package/src/ui/modifiers/border.ts +0 -100
  257. package/src/ui/modifiers/fill.ts +0 -28
  258. package/src/ui/modifiers/frame.ts +0 -74
  259. package/src/ui/modifiers/offset.ts +0 -23
  260. package/src/ui/modifiers/opacity.ts +0 -93
  261. package/src/ui/modifiers/padding.ts +0 -53
  262. package/src/ui/modifiers/styled.ts +0 -31
  263. package/src/ui/primitives/rectangle.ts +0 -25
  264. package/src/ui/primitives/spacer.ts +0 -18
  265. package/src/ui/primitives/text.ts +0 -85
  266. package/src/ui/primitives/wrapped-text.ts +0 -131
  267. package/src/ui/shinytext.ts +0 -159
  268. package/src/ui/text/layout.ts +0 -119
  269. package/src/ui/textinput.ts +0 -496
  270. package/src/ui/view-constructors.ts +0 -96
  271. package/src/ui/wrappedtext.test.ts +0 -138
@@ -0,0 +1,33 @@
1
+ // axis-helpers.ts — Utility functions for axis-aware layout calculations
2
+ // Reduces repeated `axis === "vertical" ? size.h : size.w` patterns
3
+
4
+ import type { Axis, Size, Rect } from "../types.js"
5
+
6
+ // Re-export for backwards compatibility
7
+ export type { Axis, Size, Rect } from "../types.js"
8
+
9
+ /** Get the main-axis size (height for vertical, width for horizontal) */
10
+ export const mainSize = (axis: Axis, s: Size): number => (axis === "vertical" ? s.h : s.w)
11
+
12
+ /** Get the cross-axis size (width for vertical, height for horizontal) */
13
+ export const crossSize = (axis: Axis, s: Size): number => (axis === "vertical" ? s.w : s.h)
14
+
15
+ /** Get the main-axis position (y for vertical, x for horizontal) */
16
+ export const mainPos = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.y : r.x)
17
+
18
+ /** Get the cross-axis position (x for vertical, y for horizontal) */
19
+ export const crossPos = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.x : r.y)
20
+
21
+ /** Get the main-axis dimension (height for vertical, width for horizontal) */
22
+ export const mainDim = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.h : r.w)
23
+
24
+ /** Get the cross-axis dimension (width for vertical, height for horizontal) */
25
+ export const crossDim = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.w : r.h)
26
+
27
+ /** Create a rect from main/cross coordinates and dimensions */
28
+ export const makeRect = (axis: Axis, mainP: number, crossP: number, mainD: number, crossD: number): Rect =>
29
+ axis === "vertical" ? { x: crossP, y: mainP, w: crossD, h: mainD } : { x: mainP, y: crossP, w: mainD, h: crossD }
30
+
31
+ /** Split max constraints into [maxMain, maxCross] */
32
+ export const splitConstraints = (axis: Axis, maxW: number, maxH: number): [number, number] =>
33
+ axis === "vertical" ? [maxH, maxW] : [maxW, maxH]
package/src/output.ts ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Output helpers for rendering CellBuffer to ANSI strings.
3
+ * Provides diffing, row emission, and buffer-to-string conversion.
4
+ */
5
+
6
+ import type { CellBuffer } from "./render/buffer.js"
7
+ import type { Palette } from "./render/palette.js"
8
+
9
+ /**
10
+ * Emit a row of cells as an ANSI string with run-length SGR encoding.
11
+ * Handles wide characters (cellWidth=0 continuations) and style changes.
12
+ *
13
+ * @param buffer - The cell buffer to read from
14
+ * @param palette - Palette for SGR code generation
15
+ * @param y - Row index
16
+ * @param width - Terminal width
17
+ * @param startX - Start column (default 0)
18
+ * @param endX - End column exclusive (default width)
19
+ * @returns ANSI string for the row (no cursor positioning, no trailing reset)
20
+ */
21
+ export function emitRow(
22
+ buffer: CellBuffer,
23
+ palette: Palette,
24
+ y: number,
25
+ width: number,
26
+ startX = 0,
27
+ endX = width,
28
+ ): { output: string; lastStyle: number } {
29
+ const row = y * width
30
+ let output = ""
31
+ let visualCol = startX
32
+ let currentStyle = -1
33
+
34
+ for (let x = startX; x < endX && visualCol < width; x++) {
35
+ const idx = row + x
36
+ const glyph = buffer.g[idx]
37
+ const styleId = buffer.s[idx]
38
+ const cellWidth = buffer.cw[idx] || 1
39
+
40
+ // Skip continuation cells (wide char second half)
41
+ if (cellWidth === 0) continue
42
+
43
+ // Stop if this char would overflow
44
+ if (visualCol + cellWidth > width) break
45
+
46
+ // Emit SGR only when style changes (run-length encoding)
47
+ if (styleId !== currentStyle) {
48
+ output += palette.sgr(styleId)
49
+ currentStyle = styleId
50
+ }
51
+
52
+ output += glyph === 32 ? " " : String.fromCodePoint(glyph)
53
+ visualCol += cellWidth
54
+ }
55
+
56
+ return { output, lastStyle: currentStyle }
57
+ }
58
+
59
+ /**
60
+ * Emit a row and reset style if needed.
61
+ */
62
+ export function emitRowWithReset(
63
+ buffer: CellBuffer,
64
+ palette: Palette,
65
+ y: number,
66
+ width: number,
67
+ startX = 0,
68
+ endX = width,
69
+ ): string {
70
+ const { output, lastStyle } = emitRow(buffer, palette, y, width, startX, endX)
71
+ return lastStyle !== 0 ? output + palette.sgr(0) : output
72
+ }
73
+
74
+ /**
75
+ * Check if a row changed between two buffers.
76
+ */
77
+ export function rowChanged(prev: CellBuffer, next: CellBuffer, y: number, width: number): boolean {
78
+ const row = y * width
79
+ for (let x = 0; x < width; x++) {
80
+ const idx = row + x
81
+ if (next.g[idx] !== prev.g[idx] || next.s[idx] !== prev.s[idx] || next.cw[idx] !== prev.cw[idx]) {
82
+ return true
83
+ }
84
+ }
85
+ return false
86
+ }
87
+
88
+ /**
89
+ * Find the rightmost column with content (non-space or styled).
90
+ * Returns 0 if row is empty.
91
+ */
92
+ export function rowContentWidth(buffer: CellBuffer, y: number, width: number): number {
93
+ const row = y * width
94
+ for (let x = width - 1; x >= 0; x--) {
95
+ const idx = row + x
96
+ // Skip continuation cells
97
+ if (buffer.cw[idx] === 0) continue
98
+ // Found content if non-space or has style
99
+ if (buffer.g[idx] !== 32 || buffer.s[idx] !== 0) {
100
+ return x + buffer.cw[idx] // account for wide char width
101
+ }
102
+ }
103
+ return 0
104
+ }
105
+
106
+ /**
107
+ * Find the change window between two buffers for a row.
108
+ * Returns the leftmost and rightmost changed columns, or null if no changes.
109
+ */
110
+ export function findChangeWindow(
111
+ prev: CellBuffer,
112
+ next: CellBuffer,
113
+ y: number,
114
+ width: number,
115
+ ): { left: number; right: number } | null {
116
+ const row = y * width
117
+ let left = 0
118
+ let right = width - 1
119
+
120
+ // Find leftmost change
121
+ while (left <= right) {
122
+ const i = row + left
123
+ if (next.g[i] !== prev.g[i] || next.s[i] !== prev.s[i] || next.cw[i] !== prev.cw[i]) {
124
+ break
125
+ }
126
+ left++
127
+ }
128
+
129
+ // No changes found
130
+ if (left > right) return null
131
+
132
+ // Find rightmost change
133
+ while (right >= left) {
134
+ const i = row + right
135
+ if (next.g[i] !== prev.g[i] || next.s[i] !== prev.s[i] || next.cw[i] !== prev.cw[i]) {
136
+ break
137
+ }
138
+ right--
139
+ }
140
+
141
+ return { left, right }
142
+ }
143
+
144
+ /**
145
+ * Find the last row with content in a buffer.
146
+ */
147
+ export function contentHeight(buffer: CellBuffer, width: number, height: number): number {
148
+ for (let y = height - 1; y >= 0; y--) {
149
+ const row = y * width
150
+ for (let x = 0; x < width; x++) {
151
+ const glyph = buffer.g[row + x]
152
+ const style = buffer.s[row + x]
153
+ if (glyph !== 32 || style !== 0) return y + 1
154
+ }
155
+ }
156
+ return 0
157
+ }
158
+
159
+ /**
160
+ * Convert a CellBuffer to a complete ANSI string (for static output).
161
+ * Each row is emitted with proper styling and joined with newlines.
162
+ *
163
+ * @param buffer - The cell buffer to convert
164
+ * @param palette - Palette for SGR code generation
165
+ * @param width - Buffer width
166
+ * @param height - Number of rows to emit
167
+ * @returns ANSI string with newlines between rows
168
+ */
169
+ export function bufferToString(buffer: CellBuffer, palette: Palette, width: number, height: number): string {
170
+ const lines: string[] = []
171
+ for (let y = 0; y < height; y++) {
172
+ lines.push(emitRowWithReset(buffer, palette, y, width))
173
+ }
174
+ return lines.join("\n")
175
+ }
@@ -1,5 +1,7 @@
1
1
  /* buffer.ts — 2D cell buffer with drawing, clipping, and offsets */
2
2
 
3
+ import { iterateGraphemeCells } from "./graphemes.js"
4
+
3
5
  export type Wcwidth = (cp: number) => 0 | 1 | 2
4
6
 
5
7
  // basic ASCII/Latin fallback; plug a real wcwidth for CJK/emoji later
@@ -9,192 +11,167 @@ type ClipRect = { x: number; y: number; w: number; h: number }
9
11
 
10
12
  // Represents a single frame of cells (glyph + style + cell width)
11
13
  export class CellBuffer {
12
- readonly w: number
13
- readonly h: number
14
-
15
- // cell data
16
- readonly g: Uint32Array // glyph code points
17
- readonly s: Uint32Array // style ids
18
- readonly cw: Uint8Array // cell width: 0 (continuation), 1, 2
19
-
20
- private wcwidth: Wcwidth
21
- private clip: ClipRect
22
- private clipStack: ClipRect[] = []
23
- private offX = 0
24
- private offY = 0
25
- private offStack: { x: number; y: number }[] = []
26
-
27
- constructor(width: number, height: number, wcwidth: Wcwidth = defaultWcwidth) {
28
- this.w = Math.max(1, width | 0)
29
- this.h = Math.max(1, height | 0)
30
- const n = this.w * this.h
31
- this.g = new Uint32Array(n)
32
- this.s = new Uint32Array(n)
33
- this.cw = new Uint8Array(n)
34
- this.wcwidth = wcwidth
35
- this.clip = { x: 0, y: 0, w: this.w, h: this.h }
36
- this.clear(0)
37
- }
38
-
39
- /** Clear the buffer to spaces in the given style. */
40
- clear(styleId = 0): void {
41
- this.g.fill(32)
42
- this.s.fill(styleId)
43
- this.cw.fill(1)
44
- }
45
-
46
- /** Draw a single code point at (x,y). If width=2, marks following cell as continuation. */
47
- drawCP(x: number, y: number, cp: number, styleId = 0): void {
48
- const ax = x + this.offX
49
- const ay = y + this.offY
50
- if (ax < 0 || ay < 0 || ax >= this.w || ay >= this.h) return
51
- if (ax < this.clip.x || ay < this.clip.y || ax >= this.clip.x + this.clip.w || ay >= this.clip.y + this.clip.h)
52
- return
53
- const idx = ay * this.w + ax
54
- const w = this.wcwidth(cp)
55
- this.g[idx] = cp
56
- this.s[idx] = styleId
57
- this.cw[idx] = w === 0 ? 1 : w // treat nonspacing as width 1 here
58
-
59
- if (w === 2) {
60
- // mark the continuation cell
61
- if (x + 1 < this.w) {
62
- const cidx = idx + 1
63
- this.g[cidx] = 0 // not printed
64
- this.s[cidx] = styleId
65
- this.cw[cidx] = 0 // continuation slot
66
- }
67
- }
68
- }
69
-
70
- /** Draw plain text (no wrapping) clipped to width. Uses Intl.Segmenter if available. */
71
- drawText(x: number, y: number, text: string, styleId = 0, maxWidth?: number): void {
72
- const ax = x + this.offX
73
- const ay = y + this.offY
74
- if (ay < 0 || ay >= this.h) return
75
- // vertical clip
76
- if (ay < this.clip.y || ay >= this.clip.y + this.clip.h) return
77
- const limit = Math.min(this.w - ax, maxWidth ?? this.w, this.clip.x + this.clip.w - ax)
78
- if (limit <= 0) return
79
-
80
- // iterate graphemes (so emoji / ZWJ don’t split). Fallback: naive code points.
81
- // Use Intl.Segmenter if available without importing its types to satisfy TS in Node
82
- type GraphemeSegmenter = {
83
- segment(input: string): Iterable<{ segment: string }>
84
- }
85
- const SegCtor: { new (...args: any[]): GraphemeSegmenter } | undefined = (globalThis as any).Intl?.Segmenter
86
- const seg: GraphemeSegmenter | null =
87
- typeof SegCtor === "function"
88
- ? (new (SegCtor as new (...args: any[]) => GraphemeSegmenter)(undefined, {
89
- granularity: "grapheme",
90
- }) as GraphemeSegmenter)
91
- : null
14
+ readonly w: number
15
+ readonly h: number
16
+
17
+ // cell data
18
+ readonly g: Uint32Array // glyph code points
19
+ readonly s: Uint32Array // style ids
20
+ readonly cw: Uint8Array // cell width: 0 (continuation), 1, 2
21
+
22
+ private wcwidth: Wcwidth
23
+ private clip: ClipRect
24
+ private clipStack: ClipRect[] = []
25
+ private offX = 0
26
+ private offY = 0
27
+ private offStack: { x: number; y: number }[] = []
28
+
29
+ constructor(width: number, height: number, wcwidth: Wcwidth = defaultWcwidth) {
30
+ this.w = Math.max(1, width | 0)
31
+ this.h = Math.max(1, height | 0)
32
+ const n = this.w * this.h
33
+ this.g = new Uint32Array(n)
34
+ this.s = new Uint32Array(n)
35
+ this.cw = new Uint8Array(n)
36
+ this.wcwidth = wcwidth
37
+ this.clip = { x: 0, y: 0, w: this.w, h: this.h }
38
+ this.clear(0)
39
+ }
40
+
41
+ /** Clear the buffer to spaces in the given style. */
42
+ clear(styleId = 0): void {
43
+ this.g.fill(32)
44
+ this.s.fill(styleId)
45
+ this.cw.fill(1)
46
+ }
47
+
48
+ /** Draw a single code point at (x,y). If width=2, marks following cell as continuation. */
49
+ drawCP(x: number, y: number, cp: number, styleId = 0): void {
50
+ const ax = x + this.offX
51
+ const ay = y + this.offY
52
+ if (ax < 0 || ay < 0 || ax >= this.w || ay >= this.h) return
53
+ if (ax < this.clip.x || ay < this.clip.y || ax >= this.clip.x + this.clip.w || ay >= this.clip.y + this.clip.h)
54
+ return
55
+ const idx = ay * this.w + ax
56
+ const w = this.wcwidth(cp)
57
+ this.g[idx] = cp
58
+ this.s[idx] = styleId
59
+ this.cw[idx] = w === 0 ? 1 : w // treat nonspacing as width 1 here
60
+
61
+ if (w === 2) {
62
+ // mark the continuation cell
63
+ if (x + 1 < this.w) {
64
+ const cidx = idx + 1
65
+ this.g[cidx] = 0 // not printed
66
+ this.s[cidx] = styleId
67
+ this.cw[cidx] = 0 // continuation slot
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Draw plain text (no wrapping) clipped to width. Uses Intl.Segmenter if available. */
73
+ drawText(x: number, y: number, text: string, styleId = 0, maxWidth?: number): void {
74
+ const ax = x + this.offX
75
+ const ay = y + this.offY
76
+ if (ay < 0 || ay >= this.h) return
77
+ // vertical clip
78
+ if (ay < this.clip.y || ay >= this.clip.y + this.clip.h) return
79
+ const limit = Math.min(this.w - ax, maxWidth ?? this.w, this.clip.x + this.clip.w - ax)
80
+ if (limit <= 0) return
92
81
 
93
82
  let col = 0
94
- if (seg) {
95
- for (const { segment } of seg.segment(text)) {
96
- const cp = segment.codePointAt(0) ?? 32 // first code point of grapheme
97
- const w = this.wcwidth(cp) || 1 // nonspacing combining: treat as 1 (simple path)
98
- if (col + w > limit) break
99
- this.drawCP(x + col, y, cp, styleId)
100
- col += w
101
- }
102
- } else {
103
- for (const ch of text) {
104
- const cp = ch.codePointAt(0) ?? 32
105
- const w = this.wcwidth(cp) || 1
106
- if (col + w > limit) break
107
- this.drawCP(x + col, y, cp, styleId)
108
- col += w
109
- }
83
+ for (const { cp, width } of iterateGraphemeCells(text, this.wcwidth)) {
84
+ if (col + width > limit) break
85
+ this.drawCP(x + col, y, cp, styleId)
86
+ col += width
110
87
  }
111
88
 
112
- // pad remainder within the visible limit with spaces in the same style (erases leftovers)
113
- for (; col < limit; col++) {
114
- this.drawCP(x + col, y, 32, styleId)
115
- }
116
- }
117
-
118
- /** Fill a rectangle with a code point + style. */
119
- fillRect(x: number, y: number, w: number, h: number, cp = 32, styleId = 0): void {
120
- const ax = x + this.offX
121
- const ay = y + this.offY
122
- let x0 = Math.max(0, ax | 0),
123
- y0 = Math.max(0, ay | 0)
124
- let x1 = Math.min(this.w, x0 + Math.max(0, w | 0))
125
- let y1 = Math.min(this.h, y0 + Math.max(0, h | 0))
126
- // intersect with current clip
127
- x0 = Math.max(x0, this.clip.x)
128
- y0 = Math.max(y0, this.clip.y)
129
- x1 = Math.min(x1, this.clip.x + this.clip.w)
130
- y1 = Math.min(y1, this.clip.y + this.clip.h)
131
- const width = x1 - x0
132
- if (width <= 0 || y1 <= y0) return
133
- for (let yy = y0; yy < y1; yy++) {
134
- const base = yy * this.w + x0
135
- this.g.fill(cp, base, base + width)
136
- this.s.fill(styleId, base, base + width)
137
- this.cw.fill(1, base, base + width)
138
- }
139
- }
140
-
141
- // Clipping API
142
- pushClip(x: number, y: number, w: number, h: number): void {
143
- const nx = Math.max(0, x | 0)
144
- const ny = Math.max(0, y | 0)
145
- const nw = Math.max(0, w | 0)
146
- const nh = Math.max(0, h | 0)
147
- const cur = this.clip
148
- const ix = Math.max(cur.x, nx)
149
- const iy = Math.max(cur.y, ny)
150
- const ix2 = Math.min(cur.x + cur.w, nx + nw)
151
- const iy2 = Math.min(cur.y + cur.h, ny + nh)
152
- this.clipStack.push(cur)
153
- this.clip = { x: ix, y: iy, w: Math.max(0, ix2 - ix), h: Math.max(0, iy2 - iy) }
154
- }
155
-
156
- popClip(): void {
157
- if (this.clipStack.length > 0) {
158
- const prev = this.clipStack.pop() as ClipRect
159
- this.clip = prev
160
- } else {
161
- this.clip = { x: 0, y: 0, w: this.w, h: this.h }
162
- }
163
- }
164
-
165
- withClip(x: number, y: number, w: number, h: number, fn: () => void): void {
166
- this.pushClip(x, y, w, h)
167
- try {
168
- fn()
169
- } finally {
170
- this.popClip()
171
- }
172
- }
173
-
174
- // Translation API
175
- pushOffset(dx: number, dy: number): void {
176
- this.offStack.push({ x: this.offX, y: this.offY })
177
- this.offX += dx | 0
178
- this.offY += dy | 0
179
- }
180
-
181
- popOffset(): void {
182
- if (this.offStack.length > 0) {
183
- const prev = this.offStack.pop() as { x: number; y: number }
184
- this.offX = prev.x
185
- this.offY = prev.y
186
- } else {
187
- this.offX = 0
188
- this.offY = 0
189
- }
190
- }
191
-
192
- withOffset(dx: number, dy: number, fn: () => void): void {
193
- this.pushOffset(dx, dy)
194
- try {
195
- fn()
196
- } finally {
197
- this.popOffset()
198
- }
199
- }
89
+ // pad remainder within the visible limit with spaces in the same style (erases leftovers)
90
+ for (; col < limit; col++) {
91
+ this.drawCP(x + col, y, 32, styleId)
92
+ }
93
+ }
94
+
95
+ /** Fill a rectangle with a code point + style. */
96
+ fillRect(x: number, y: number, w: number, h: number, cp = 32, styleId = 0): void {
97
+ const ax = x + this.offX
98
+ const ay = y + this.offY
99
+ let x0 = Math.max(0, ax | 0),
100
+ y0 = Math.max(0, ay | 0)
101
+ let x1 = Math.min(this.w, x0 + Math.max(0, w | 0))
102
+ let y1 = Math.min(this.h, y0 + Math.max(0, h | 0))
103
+ // intersect with current clip
104
+ x0 = Math.max(x0, this.clip.x)
105
+ y0 = Math.max(y0, this.clip.y)
106
+ x1 = Math.min(x1, this.clip.x + this.clip.w)
107
+ y1 = Math.min(y1, this.clip.y + this.clip.h)
108
+ const width = x1 - x0
109
+ if (width <= 0 || y1 <= y0) return
110
+ for (let yy = y0; yy < y1; yy++) {
111
+ const base = yy * this.w + x0
112
+ this.g.fill(cp, base, base + width)
113
+ this.s.fill(styleId, base, base + width)
114
+ this.cw.fill(1, base, base + width)
115
+ }
116
+ }
117
+
118
+ // Clipping API
119
+ pushClip(x: number, y: number, w: number, h: number): void {
120
+ const nx = Math.max(0, x | 0)
121
+ const ny = Math.max(0, y | 0)
122
+ const nw = Math.max(0, w | 0)
123
+ const nh = Math.max(0, h | 0)
124
+ const cur = this.clip
125
+ const ix = Math.max(cur.x, nx)
126
+ const iy = Math.max(cur.y, ny)
127
+ const ix2 = Math.min(cur.x + cur.w, nx + nw)
128
+ const iy2 = Math.min(cur.y + cur.h, ny + nh)
129
+ this.clipStack.push(cur)
130
+ this.clip = { x: ix, y: iy, w: Math.max(0, ix2 - ix), h: Math.max(0, iy2 - iy) }
131
+ }
132
+
133
+ popClip(): void {
134
+ if (this.clipStack.length > 0) {
135
+ const prev = this.clipStack.pop() as ClipRect
136
+ this.clip = prev
137
+ } else {
138
+ this.clip = { x: 0, y: 0, w: this.w, h: this.h }
139
+ }
140
+ }
141
+
142
+ withClip(x: number, y: number, w: number, h: number, fn: () => void): void {
143
+ this.pushClip(x, y, w, h)
144
+ try {
145
+ fn()
146
+ } finally {
147
+ this.popClip()
148
+ }
149
+ }
150
+
151
+ // Translation API
152
+ pushOffset(dx: number, dy: number): void {
153
+ this.offStack.push({ x: this.offX, y: this.offY })
154
+ this.offX += dx | 0
155
+ this.offY += dy | 0
156
+ }
157
+
158
+ popOffset(): void {
159
+ if (this.offStack.length > 0) {
160
+ const prev = this.offStack.pop() as { x: number; y: number }
161
+ this.offX = prev.x
162
+ this.offY = prev.y
163
+ } else {
164
+ this.offX = 0
165
+ this.offY = 0
166
+ }
167
+ }
168
+
169
+ withOffset(dx: number, dy: number, fn: () => void): void {
170
+ this.pushOffset(dx, dy)
171
+ try {
172
+ fn()
173
+ } finally {
174
+ this.popOffset()
175
+ }
176
+ }
200
177
  }
@@ -0,0 +1,34 @@
1
+ import { getGraphemeSegmenter } from "./segmenter.js"
2
+ import type { Wcwidth } from "./buffer.js"
3
+
4
+ /**
5
+ * Iterate grapheme clusters using Intl.Segmenter when available.
6
+ * Falls back to code point iteration when not supported.
7
+ */
8
+ export function* iterateGraphemes(text: string): IterableIterator<string> {
9
+ const seg = getGraphemeSegmenter()
10
+ if (seg) {
11
+ for (const { segment } of seg.segment(text)) {
12
+ yield segment
13
+ }
14
+ return
15
+ }
16
+
17
+ for (const ch of text) {
18
+ yield ch
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Iterate grapheme clusters and provide their first code point + display width.
24
+ */
25
+ export function* iterateGraphemeCells(
26
+ text: string,
27
+ wcwidth: Wcwidth,
28
+ ): IterableIterator<{ segment: string; cp: number; width: number }> {
29
+ for (const segment of iterateGraphemes(text)) {
30
+ const cp = segment.codePointAt(0) ?? 32
31
+ const width = wcwidth(cp) || 1
32
+ yield { segment, cp, width }
33
+ }
34
+ }
@@ -7,45 +7,21 @@
7
7
 
8
8
  import type { Wcwidth } from "./buffer.js"
9
9
  import { defaultWcwidth } from "./buffer.js"
10
+ import { iterateGraphemes, iterateGraphemeCells } from "./graphemes.js"
10
11
 
11
- type GraphemeSegmenter = {
12
- segment(input: string): Iterable<{ segment: string }>
13
- }
14
-
15
- function getGraphemeSegmenter(): GraphemeSegmenter | null {
16
- const SegCtor: { new (...args: any[]): GraphemeSegmenter } | undefined = (globalThis as any).Intl?.Segmenter
17
- if (typeof SegCtor === "function") {
18
- try {
19
- return new (SegCtor as new (...args: any[]) => GraphemeSegmenter)(undefined, {
20
- granularity: "grapheme",
21
- }) as GraphemeSegmenter
22
- } catch {
23
- // ignore and fall back
24
- }
25
- }
26
- return null
27
- }
12
+ // Re-export for backwards compatibility
13
+ export { getGraphemeSegmenter, type GraphemeSegmenter } from "./segmenter.js"
28
14
 
29
15
  /** Split a string into grapheme clusters (ZWJ sequences kept together when supported). */
30
16
  export function graphemes(text: string): string[] {
31
- const seg = getGraphemeSegmenter()
32
- if (seg) {
33
- const out: string[] = []
34
- for (const { segment } of seg.segment(text)) out.push(segment)
35
- return out
36
- }
37
- // Fallback: iterate code points (may split ZWJ sequences)
38
- return Array.from(text)
17
+ return Array.from(iterateGraphemes(text))
39
18
  }
40
19
 
41
20
  /** Compute display width using wcwidth of the first code point of each grapheme. */
42
21
  export function displayWidth(text: string, wc: Wcwidth = defaultWcwidth): number {
43
22
  let w = 0
44
- const gs = graphemes(text)
45
- for (const g of gs) {
46
- const cp = g.codePointAt(0) ?? 32
47
- const ww = wc(cp) || 1
48
- w += ww
23
+ for (const { width } of iterateGraphemeCells(text, wc)) {
24
+ w += width
49
25
  }
50
26
  return w
51
27
  }
@@ -60,15 +36,16 @@ export function sliceByWidth(
60
36
  wc: Wcwidth = defaultWcwidth,
61
37
  ): { text: string; width: number; complete: boolean } {
62
38
  if (maxWidth <= 0) return { text: "", width: 0, complete: text.length === 0 }
63
- const gs = graphemes(text)
64
39
  const out: string[] = []
65
40
  let used = 0
66
- for (const g of gs) {
67
- const cp = g.codePointAt(0) ?? 32
68
- const ww = wc(cp) || 1
69
- if (used + ww > maxWidth) break
70
- out.push(g)
71
- used += ww
41
+ let complete = true
42
+ for (const { segment, width } of iterateGraphemeCells(text, wc)) {
43
+ if (used + width > maxWidth) {
44
+ complete = false
45
+ break
46
+ }
47
+ out.push(segment)
48
+ used += width
72
49
  }
73
- return { text: out.join(""), width: used, complete: out.length === gs.length }
50
+ return { text: out.join(""), width: used, complete }
74
51
  }