@effect-tui/core 0.1.0-alpha.1

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 (241) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/anim.d.ts +4 -0
  4. package/dist/anim.d.ts.map +1 -0
  5. package/dist/anim.js +5 -0
  6. package/dist/anim.js.map +1 -0
  7. package/dist/ansi.d.ts +69 -0
  8. package/dist/ansi.d.ts.map +1 -0
  9. package/dist/ansi.js +72 -0
  10. package/dist/ansi.js.map +1 -0
  11. package/dist/index.d.ts +16 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +16 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/keys.d.ts +18 -0
  16. package/dist/keys.d.ts.map +1 -0
  17. package/dist/keys.js +247 -0
  18. package/dist/keys.js.map +1 -0
  19. package/dist/layout/linearStack.d.ts +17 -0
  20. package/dist/layout/linearStack.d.ts.map +1 -0
  21. package/dist/layout/linearStack.js +86 -0
  22. package/dist/layout/linearStack.js.map +1 -0
  23. package/dist/motion-value.d.ts +58 -0
  24. package/dist/motion-value.d.ts.map +1 -0
  25. package/dist/motion-value.js +250 -0
  26. package/dist/motion-value.js.map +1 -0
  27. package/dist/present/display.d.ts +58 -0
  28. package/dist/present/display.d.ts.map +1 -0
  29. package/dist/present/display.js +168 -0
  30. package/dist/present/display.js.map +1 -0
  31. package/dist/present/writers/fullscreen.d.ts +19 -0
  32. package/dist/present/writers/fullscreen.d.ts.map +1 -0
  33. package/dist/present/writers/fullscreen.js +55 -0
  34. package/dist/present/writers/fullscreen.js.map +1 -0
  35. package/dist/present/writers/inline.d.ts +20 -0
  36. package/dist/present/writers/inline.d.ts.map +1 -0
  37. package/dist/present/writers/inline.js +92 -0
  38. package/dist/present/writers/inline.js.map +1 -0
  39. package/dist/render/buffer.d.ts +31 -0
  40. package/dist/render/buffer.d.ts.map +1 -0
  41. package/dist/render/buffer.js +183 -0
  42. package/dist/render/buffer.js.map +1 -0
  43. package/dist/render/color-utils.d.ts +18 -0
  44. package/dist/render/color-utils.d.ts.map +1 -0
  45. package/dist/render/color-utils.js +58 -0
  46. package/dist/render/color-utils.js.map +1 -0
  47. package/dist/render/diff.d.ts +30 -0
  48. package/dist/render/diff.d.ts.map +1 -0
  49. package/dist/render/diff.js +83 -0
  50. package/dist/render/diff.js.map +1 -0
  51. package/dist/render/measure.d.ts +15 -0
  52. package/dist/render/measure.d.ts.map +1 -0
  53. package/dist/render/measure.js +65 -0
  54. package/dist/render/measure.js.map +1 -0
  55. package/dist/render/palette.d.ts +46 -0
  56. package/dist/render/palette.d.ts.map +1 -0
  57. package/dist/render/palette.js +108 -0
  58. package/dist/render/palette.js.map +1 -0
  59. package/dist/render/surface.d.ts +77 -0
  60. package/dist/render/surface.d.ts.map +1 -0
  61. package/dist/render/surface.js +198 -0
  62. package/dist/render/surface.js.map +1 -0
  63. package/dist/runtime/backend_node.d.ts +36 -0
  64. package/dist/runtime/backend_node.d.ts.map +1 -0
  65. package/dist/runtime/backend_node.js +66 -0
  66. package/dist/runtime/backend_node.js.map +1 -0
  67. package/dist/spring-physics.d.ts +36 -0
  68. package/dist/spring-physics.d.ts.map +1 -0
  69. package/dist/spring-physics.js +113 -0
  70. package/dist/spring-physics.js.map +1 -0
  71. package/dist/spring.d.ts +73 -0
  72. package/dist/spring.d.ts.map +1 -0
  73. package/dist/spring.js +136 -0
  74. package/dist/spring.js.map +1 -0
  75. package/dist/ui/containers/canvas.d.ts +13 -0
  76. package/dist/ui/containers/canvas.d.ts.map +1 -0
  77. package/dist/ui/containers/canvas.js +16 -0
  78. package/dist/ui/containers/canvas.js.map +1 -0
  79. package/dist/ui/containers/geometry-reader.d.ts +17 -0
  80. package/dist/ui/containers/geometry-reader.d.ts.map +1 -0
  81. package/dist/ui/containers/geometry-reader.js +24 -0
  82. package/dist/ui/containers/geometry-reader.js.map +1 -0
  83. package/dist/ui/containers/hstack.d.ts +12 -0
  84. package/dist/ui/containers/hstack.d.ts.map +1 -0
  85. package/dist/ui/containers/hstack.js +28 -0
  86. package/dist/ui/containers/hstack.js.map +1 -0
  87. package/dist/ui/containers/scroll.d.ts +28 -0
  88. package/dist/ui/containers/scroll.d.ts.map +1 -0
  89. package/dist/ui/containers/scroll.js +97 -0
  90. package/dist/ui/containers/scroll.js.map +1 -0
  91. package/dist/ui/containers/shared.d.ts +12 -0
  92. package/dist/ui/containers/shared.d.ts.map +1 -0
  93. package/dist/ui/containers/shared.js +19 -0
  94. package/dist/ui/containers/shared.js.map +1 -0
  95. package/dist/ui/containers/vstack.d.ts +12 -0
  96. package/dist/ui/containers/vstack.d.ts.map +1 -0
  97. package/dist/ui/containers/vstack.js +28 -0
  98. package/dist/ui/containers/vstack.js.map +1 -0
  99. package/dist/ui/containers/zstack.d.ts +14 -0
  100. package/dist/ui/containers/zstack.d.ts.map +1 -0
  101. package/dist/ui/containers/zstack.js +36 -0
  102. package/dist/ui/containers/zstack.js.map +1 -0
  103. package/dist/ui/core/geometry-store.d.ts +22 -0
  104. package/dist/ui/core/geometry-store.d.ts.map +1 -0
  105. package/dist/ui/core/geometry-store.js +29 -0
  106. package/dist/ui/core/geometry-store.js.map +1 -0
  107. package/dist/ui/core/geometry.d.ts +34 -0
  108. package/dist/ui/core/geometry.d.ts.map +1 -0
  109. package/dist/ui/core/geometry.js +14 -0
  110. package/dist/ui/core/geometry.js.map +1 -0
  111. package/dist/ui/core/view.d.ts +25 -0
  112. package/dist/ui/core/view.d.ts.map +1 -0
  113. package/dist/ui/core/view.js +34 -0
  114. package/dist/ui/core/view.js.map +1 -0
  115. package/dist/ui/index.d.ts +44 -0
  116. package/dist/ui/index.d.ts.map +1 -0
  117. package/dist/ui/index.js +39 -0
  118. package/dist/ui/index.js.map +1 -0
  119. package/dist/ui/inlinetext.d.ts +24 -0
  120. package/dist/ui/inlinetext.d.ts.map +1 -0
  121. package/dist/ui/inlinetext.js +131 -0
  122. package/dist/ui/inlinetext.js.map +1 -0
  123. package/dist/ui/install.d.ts +22 -0
  124. package/dist/ui/install.d.ts.map +1 -0
  125. package/dist/ui/install.js +66 -0
  126. package/dist/ui/install.js.map +1 -0
  127. package/dist/ui/markdown.d.ts +40 -0
  128. package/dist/ui/markdown.d.ts.map +1 -0
  129. package/dist/ui/markdown.js +351 -0
  130. package/dist/ui/markdown.js.map +1 -0
  131. package/dist/ui/modifiers/border.d.ts +33 -0
  132. package/dist/ui/modifiers/border.d.ts.map +1 -0
  133. package/dist/ui/modifiers/border.js +82 -0
  134. package/dist/ui/modifiers/border.js.map +1 -0
  135. package/dist/ui/modifiers/fill.d.ts +14 -0
  136. package/dist/ui/modifiers/fill.d.ts.map +1 -0
  137. package/dist/ui/modifiers/fill.js +25 -0
  138. package/dist/ui/modifiers/fill.js.map +1 -0
  139. package/dist/ui/modifiers/frame.d.ts +23 -0
  140. package/dist/ui/modifiers/frame.d.ts.map +1 -0
  141. package/dist/ui/modifiers/frame.js +54 -0
  142. package/dist/ui/modifiers/frame.js.map +1 -0
  143. package/dist/ui/modifiers/offset.d.ts +15 -0
  144. package/dist/ui/modifiers/offset.d.ts.map +1 -0
  145. package/dist/ui/modifiers/offset.js +21 -0
  146. package/dist/ui/modifiers/offset.js.map +1 -0
  147. package/dist/ui/modifiers/opacity.d.ts +15 -0
  148. package/dist/ui/modifiers/opacity.d.ts.map +1 -0
  149. package/dist/ui/modifiers/opacity.js +95 -0
  150. package/dist/ui/modifiers/opacity.js.map +1 -0
  151. package/dist/ui/modifiers/padding.d.ts +20 -0
  152. package/dist/ui/modifiers/padding.d.ts.map +1 -0
  153. package/dist/ui/modifiers/padding.js +36 -0
  154. package/dist/ui/modifiers/padding.js.map +1 -0
  155. package/dist/ui/modifiers/styled.d.ts +14 -0
  156. package/dist/ui/modifiers/styled.d.ts.map +1 -0
  157. package/dist/ui/modifiers/styled.js +26 -0
  158. package/dist/ui/modifiers/styled.js.map +1 -0
  159. package/dist/ui/primitives/rectangle.d.ts +15 -0
  160. package/dist/ui/primitives/rectangle.d.ts.map +1 -0
  161. package/dist/ui/primitives/rectangle.js +23 -0
  162. package/dist/ui/primitives/rectangle.js.map +1 -0
  163. package/dist/ui/primitives/spacer.d.ts +13 -0
  164. package/dist/ui/primitives/spacer.d.ts.map +1 -0
  165. package/dist/ui/primitives/spacer.js +16 -0
  166. package/dist/ui/primitives/spacer.js.map +1 -0
  167. package/dist/ui/primitives/text.d.ts +15 -0
  168. package/dist/ui/primitives/text.d.ts.map +1 -0
  169. package/dist/ui/primitives/text.js +79 -0
  170. package/dist/ui/primitives/text.js.map +1 -0
  171. package/dist/ui/primitives/wrapped-text.d.ts +30 -0
  172. package/dist/ui/primitives/wrapped-text.d.ts.map +1 -0
  173. package/dist/ui/primitives/wrapped-text.js +117 -0
  174. package/dist/ui/primitives/wrapped-text.js.map +1 -0
  175. package/dist/ui/shinytext.d.ts +66 -0
  176. package/dist/ui/shinytext.d.ts.map +1 -0
  177. package/dist/ui/shinytext.js +99 -0
  178. package/dist/ui/shinytext.js.map +1 -0
  179. package/dist/ui/text/layout.d.ts +35 -0
  180. package/dist/ui/text/layout.d.ts.map +1 -0
  181. package/dist/ui/text/layout.js +102 -0
  182. package/dist/ui/text/layout.js.map +1 -0
  183. package/dist/ui/textinput.d.ts +140 -0
  184. package/dist/ui/textinput.d.ts.map +1 -0
  185. package/dist/ui/textinput.js +402 -0
  186. package/dist/ui/textinput.js.map +1 -0
  187. package/dist/ui/view-constructors.d.ts +72 -0
  188. package/dist/ui/view-constructors.d.ts.map +1 -0
  189. package/dist/ui/view-constructors.js +74 -0
  190. package/dist/ui/view-constructors.js.map +1 -0
  191. package/package.json +57 -0
  192. package/src/anim.ts +5 -0
  193. package/src/ansi.ts +83 -0
  194. package/src/index.ts +21 -0
  195. package/src/keys.ts +302 -0
  196. package/src/layout/linearStack.ts +115 -0
  197. package/src/motion-value.ts +335 -0
  198. package/src/present/display.ts +206 -0
  199. package/src/present/writers/fullscreen.ts +58 -0
  200. package/src/present/writers/inline.ts +101 -0
  201. package/src/render/buffer.ts +200 -0
  202. package/src/render/color-utils.ts +60 -0
  203. package/src/render/diff.ts +95 -0
  204. package/src/render/measure.ts +74 -0
  205. package/src/render/palette.ts +113 -0
  206. package/src/render/surface.ts +238 -0
  207. package/src/runtime/backend_node.ts +80 -0
  208. package/src/spring-physics.ts +151 -0
  209. package/src/spring.ts +234 -0
  210. package/src/ui/__snapshots__/wrappedtext.test.ts.snap +57 -0
  211. package/src/ui/containers/canvas.ts +18 -0
  212. package/src/ui/containers/geometry-reader.ts +32 -0
  213. package/src/ui/containers/hstack.ts +33 -0
  214. package/src/ui/containers/scroll.ts +106 -0
  215. package/src/ui/containers/shared.ts +27 -0
  216. package/src/ui/containers/vstack.ts +34 -0
  217. package/src/ui/containers/zstack.ts +37 -0
  218. package/src/ui/core/geometry-store.ts +42 -0
  219. package/src/ui/core/geometry.ts +30 -0
  220. package/src/ui/core/view.ts +49 -0
  221. package/src/ui/index.ts +84 -0
  222. package/src/ui/inlinetext.ts +135 -0
  223. package/src/ui/install.ts +110 -0
  224. package/src/ui/markdown.test.ts +74 -0
  225. package/src/ui/markdown.ts +388 -0
  226. package/src/ui/modifiers/border.ts +100 -0
  227. package/src/ui/modifiers/fill.ts +28 -0
  228. package/src/ui/modifiers/frame.ts +74 -0
  229. package/src/ui/modifiers/offset.ts +23 -0
  230. package/src/ui/modifiers/opacity.ts +93 -0
  231. package/src/ui/modifiers/padding.ts +53 -0
  232. package/src/ui/modifiers/styled.ts +31 -0
  233. package/src/ui/primitives/rectangle.ts +25 -0
  234. package/src/ui/primitives/spacer.ts +18 -0
  235. package/src/ui/primitives/text.ts +85 -0
  236. package/src/ui/primitives/wrapped-text.ts +131 -0
  237. package/src/ui/shinytext.ts +159 -0
  238. package/src/ui/text/layout.ts +119 -0
  239. package/src/ui/textinput.ts +496 -0
  240. package/src/ui/view-constructors.ts +96 -0
  241. package/src/ui/wrappedtext.test.ts +138 -0
@@ -0,0 +1,200 @@
1
+ /* buffer.ts — 2D cell buffer with drawing, clipping, and offsets */
2
+
3
+ export type Wcwidth = (cp: number) => 0 | 1 | 2
4
+
5
+ // basic ASCII/Latin fallback; plug a real wcwidth for CJK/emoji later
6
+ export const defaultWcwidth: Wcwidth = (cp) => (cp === 0 ? 0 : cp < 32 ? 0 : cp === 0x7f ? 0 : 1)
7
+
8
+ type ClipRect = { x: number; y: number; w: number; h: number }
9
+
10
+ // Represents a single frame of cells (glyph + style + cell width)
11
+ 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
92
+
93
+ 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
+ }
110
+ }
111
+
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
+ }
200
+ }
@@ -0,0 +1,60 @@
1
+ // color-utils.ts - Shared color conversion utilities
2
+
3
+ /**
4
+ * Clamp a number to the 0-255 range and convert to integer.
5
+ */
6
+ export function clamp255(n: number): number {
7
+ return n < 0 ? 0 : n > 255 ? 255 : n | 0
8
+ }
9
+
10
+ /**
11
+ * Convert a 256-color index to RGB values (approximate xterm palette).
12
+ *
13
+ * The 256-color palette is divided into:
14
+ * - 0-15: Standard ANSI colors (system colors)
15
+ * - 16-231: 6x6x6 RGB cube
16
+ * - 232-255: Grayscale ramp (24 shades)
17
+ */
18
+ export function idxToRGB(idx: number): { r: number; g: number; b: number } {
19
+ if (idx < 0) idx = 0
20
+ if (idx > 255) idx = 255
21
+
22
+ // Standard ANSI colors (0-15)
23
+ if (idx < 16) {
24
+ const base: Array<[number, number, number]> = [
25
+ [0x00, 0x00, 0x00], // 0: black
26
+ [0x80, 0x00, 0x00], // 1: red
27
+ [0x00, 0x80, 0x00], // 2: green
28
+ [0x80, 0x80, 0x00], // 3: yellow
29
+ [0x00, 0x00, 0x80], // 4: blue
30
+ [0x80, 0x00, 0x80], // 5: magenta
31
+ [0x00, 0x80, 0x80], // 6: cyan
32
+ [0xc0, 0xc0, 0xc0], // 7: white
33
+ [0x80, 0x80, 0x80], // 8: bright black (gray)
34
+ [0xff, 0x00, 0x00], // 9: bright red
35
+ [0x00, 0xff, 0x00], // 10: bright green
36
+ [0xff, 0xff, 0x00], // 11: bright yellow
37
+ [0x00, 0x00, 0xff], // 12: bright blue
38
+ [0xff, 0x00, 0xff], // 13: bright magenta
39
+ [0x00, 0xff, 0xff], // 14: bright cyan
40
+ [0xff, 0xff, 0xff], // 15: bright white
41
+ ]
42
+ const [r, g, b] = base[idx] ?? [0, 0, 0]
43
+ return { r, g, b }
44
+ }
45
+
46
+ // Grayscale ramp (232-255)
47
+ if (idx >= 232) {
48
+ const n = idx - 232 // 0..23
49
+ const v = 8 + n * 10
50
+ return { r: v, g: v, b: v }
51
+ }
52
+
53
+ // 6x6x6 RGB cube (16-231)
54
+ const n = idx - 16 // 0..215
55
+ const r = Math.floor(n / 36) % 6
56
+ const g = Math.floor(n / 6) % 6
57
+ const b = n % 6
58
+ const steps = [0, 95, 135, 175, 215, 255]
59
+ return { r: steps[r] ?? 0, g: steps[g] ?? 0, b: steps[b] ?? 0 }
60
+ }
@@ -0,0 +1,95 @@
1
+ import type { Surface } from "./surface.js"
2
+
3
+ /**
4
+ * Writer that consumes style runs produced by diffing two frames.
5
+ * - Coordinates are 0-based: `row` in [0..rows), `col` in [0..cols).
6
+ * - `begin` is called once with the viewport size, then `run` zero or more times,
7
+ * then `end`. Implementations may buffer and expose the result via `flush`.
8
+ */
9
+ export interface RunWriter {
10
+ begin(viewport: { cols: number; rows: number }): void
11
+ run(row: number, col: number, styleId: number, text: string): void
12
+ end(): void
13
+ flush(): string
14
+ }
15
+
16
+ /**
17
+ * Stream a minimal set of style-runs for changes from prev → next.
18
+ *
19
+ * Algorithm (row-wise):
20
+ * - For each row, compute a tight change window [left, right]. Skip entirely if equal.
21
+ * - Within the window, emit contiguous runs grouped by style. A new run begins when:
22
+ * - A cell changes, and
23
+ * - Its style differs from the current run (or the run just started).
24
+ * - Wide glyph continuation cells (cw=0) are skipped so they are not reprinted.
25
+ *
26
+ * Assumes both Surfaces are same size and compares their back buffers (B).
27
+ */
28
+ export function diffFrames(prev: Surface, next: Surface, w: RunWriter): void {
29
+ const width = next.w
30
+ const height = next.h
31
+ // Access back buffers (like Surface.flush does). B is private on Surface, so use `any`.
32
+ const gA = (prev as any).B.g as Uint32Array
33
+ const sA = (prev as any).B.s as Uint32Array
34
+ const gB = (next as any).B.g as Uint32Array
35
+ const sB = (next as any).B.s as Uint32Array
36
+ const cwB = (next as any).B.cw as Uint8Array
37
+
38
+ w.begin({ cols: width, rows: height })
39
+
40
+ for (let y = 0; y < height; y++) {
41
+ const row = y * width
42
+
43
+ // Fast skip window: [left..right] bounds that changed
44
+ let left = 0,
45
+ right = width - 1
46
+ while (left <= right) {
47
+ const i = row + left
48
+ if (gA[i] === gB[i] && sA[i] === sB[i]) left++
49
+ else break
50
+ }
51
+ if (left > right) continue
52
+
53
+ while (right >= left) {
54
+ const i = row + right
55
+ if (gA[i] === gB[i] && sA[i] === sB[i]) right--
56
+ else break
57
+ }
58
+
59
+ // Emit contiguous style runs within [left..right]
60
+ let x = left
61
+ while (x <= right) {
62
+ // Skip unchanged within window
63
+ while (x <= right) {
64
+ const i = row + x
65
+ if (gA[i] !== gB[i] || sA[i] !== sB[i]) break
66
+ x++
67
+ }
68
+ if (x > right) break
69
+
70
+ const style = sB[row + x]
71
+ const runX = x
72
+ let text = ""
73
+
74
+ while (x <= right) {
75
+ const i = row + x
76
+ if (sB[i] !== style) break
77
+ const cp = gB[i]
78
+ const ww = cwB[i]
79
+ if (ww !== 0) text += cp === 32 ? " " : String.fromCodePoint(cp)
80
+ x++
81
+ // skip continuation cells of wide glyphs from the next buffer
82
+ while (x <= right && cwB[row + x] === 0) x++
83
+
84
+ // Stop run if next cell is unchanged; diff window will reposition
85
+ if (x <= right) {
86
+ const j = row + x
87
+ if (gA[j] === gB[j] && sA[j] === sB[j]) break
88
+ }
89
+ }
90
+
91
+ if (text.length > 0) w.run(y, runX, style, text)
92
+ }
93
+ }
94
+ w.end()
95
+ }
@@ -0,0 +1,74 @@
1
+ /* measure.ts — shared grapheme segmentation and display width helpers
2
+ *
3
+ * These helpers intentionally mirror CellBuffer.drawText semantics:
4
+ * - Prefer Intl.Segmenter('grapheme') when available; otherwise iterate code points.
5
+ * - Width is derived from wcwidth(first code point) with a minimum of 1.
6
+ */
7
+
8
+ import type { Wcwidth } from "./buffer.js"
9
+ import { defaultWcwidth } from "./buffer.js"
10
+
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
+ }
28
+
29
+ /** Split a string into grapheme clusters (ZWJ sequences kept together when supported). */
30
+ 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)
39
+ }
40
+
41
+ /** Compute display width using wcwidth of the first code point of each grapheme. */
42
+ export function displayWidth(text: string, wc: Wcwidth = defaultWcwidth): number {
43
+ 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
49
+ }
50
+ return w
51
+ }
52
+
53
+ /**
54
+ * Take as many graphemes as fit within maxWidth cells.
55
+ * Returns the substring, its display width, and whether the whole input fit.
56
+ */
57
+ export function sliceByWidth(
58
+ text: string,
59
+ maxWidth: number,
60
+ wc: Wcwidth = defaultWcwidth,
61
+ ): { text: string; width: number; complete: boolean } {
62
+ if (maxWidth <= 0) return { text: "", width: 0, complete: text.length === 0 }
63
+ const gs = graphemes(text)
64
+ const out: string[] = []
65
+ 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
72
+ }
73
+ return { text: out.join(""), width: used, complete: out.length === gs.length }
74
+ }
@@ -0,0 +1,113 @@
1
+ const ESC = "\x1b["
2
+ const CSI = ESC // alias for clarity
3
+
4
+ // Style palette: intern style specs -> small numeric ids -> prebuilt SGR strings
5
+ // Color can be an xterm-256 index (0..255) or a truecolor triplet.
6
+ export type ColorValue = number | { r: number; g: number; b: number }
7
+
8
+ // Minimal set of attributes we support. Additional flags can be added as needed.
9
+ export type StyleSpec = {
10
+ fg?: ColorValue // foreground: 256 index or RGB
11
+ bg?: ColorValue // background: 256 index or RGB
12
+ bold?: boolean
13
+ italic?: boolean
14
+ underline?: boolean
15
+ inverse?: boolean
16
+ }
17
+
18
+ /**
19
+ * Palette interns "style specs" into small numeric ids and caches their
20
+ * ANSI SGR escape strings. Id 0 always represents the default terminal style.
21
+ */
22
+ export class Palette {
23
+ private nextId = 1 // 0 = default terminal style
24
+ private idByKey = new Map<string, number>()
25
+ private sgrById = new Map<number, string>()
26
+
27
+ /** Get or create a numeric style id for a given style spec. */
28
+ id(spec?: StyleSpec): number {
29
+ if (!spec) return 0
30
+ const key = JSON.stringify(spec)
31
+ let id = this.idByKey.get(key)
32
+ if (id) return id
33
+ id = this.nextId++
34
+ this.idByKey.set(key, id)
35
+ this.sgrById.set(id, this.buildSGR(spec))
36
+ return id
37
+ }
38
+
39
+ /** Look up the ANSI SGR sequence for a style id. Id 0 resets attributes. */
40
+ sgr(id: number): string {
41
+ if (id === 0) return `${ESC}0m`
42
+ const s = this.sgrById.get(id)
43
+ return s ?? `${ESC}0m`
44
+ }
45
+
46
+ /**
47
+ * Build an ANSI SGR sequence for a style spec.
48
+ * We always begin with reset (0) to avoid style drift across runs,
49
+ * then add only the requested attributes.
50
+ */
51
+ private buildSGR(spec: StyleSpec): string {
52
+ const parts: string[] = []
53
+ parts.push("0") // reset, then set exact attrs to avoid drift
54
+ if (spec.bold) parts.push("1")
55
+ if (spec.italic) parts.push("3")
56
+ if (spec.underline) parts.push("4")
57
+ if (spec.inverse) parts.push("7")
58
+
59
+ const colorToCodes = (c: ColorValue | undefined, isBg: boolean): string | undefined => {
60
+ if (c == null) return undefined
61
+ if (typeof c === "number") return (isBg ? "48;5;" : "38;5;") + String(c | 0)
62
+ const clamp = (n: number) => (n < 0 ? 0 : n > 255 ? 255 : n | 0)
63
+ return `${isBg ? "48;2;" : "38;2;"}${clamp(c.r)};${clamp(c.g)};${clamp(c.b)}`
64
+ }
65
+
66
+ const fgc = colorToCodes(spec.fg, false)
67
+ const bgc = colorToCodes(spec.bg, true)
68
+ if (fgc) parts.push(fgc)
69
+ if (bgc) parts.push(bgc)
70
+
71
+ return `${CSI}${parts.join(";")}m`
72
+ }
73
+ }
74
+
75
+ /**
76
+ * ScopedPalette provides style scoping: it merges a base style with any
77
+ * requested style, while delegating id/sgr generation to a shared Palette.
78
+ */
79
+ export class ScopedPalette extends Palette {
80
+ constructor(
81
+ private readonly base: Palette,
82
+ private readonly baseStyle?: StyleSpec,
83
+ ) {
84
+ super()
85
+ }
86
+
87
+ override id(spec?: StyleSpec): number {
88
+ return this.base.id(mergeStyle(this.baseStyle, spec))
89
+ }
90
+
91
+ override sgr(id: number): string {
92
+ return this.base.sgr(id)
93
+ }
94
+ }
95
+
96
+ /** Merge two style specs (b overrides a). Returns undefined if nothing to set. */
97
+ export function mergeStyle(a?: StyleSpec, b?: StyleSpec): StyleSpec | undefined {
98
+ if (!a && !b) return undefined
99
+ const merged: StyleSpec = {}
100
+ const fg = b?.fg ?? a?.fg
101
+ const bg = b?.bg ?? a?.bg
102
+ const bold = b?.bold ?? a?.bold
103
+ const italic = b?.italic ?? a?.italic
104
+ const underline = b?.underline ?? a?.underline
105
+ const inverse = b?.inverse ?? a?.inverse
106
+ if (fg !== undefined) merged.fg = fg
107
+ if (bg !== undefined) merged.bg = bg
108
+ if (bold !== undefined) merged.bold = bold
109
+ if (italic !== undefined) merged.italic = italic
110
+ if (underline !== undefined) merged.underline = underline
111
+ if (inverse !== undefined) merged.inverse = inverse
112
+ return Object.keys(merged).length > 0 ? merged : undefined
113
+ }