@effect-tui/core 0.1.0 → 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
@@ -7,12 +7,32 @@ export type ColorValue = number | { r: number; g: number; b: number }
7
7
 
8
8
  // Minimal set of attributes we support. Additional flags can be added as needed.
9
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
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
+ * Create a canonical key for a StyleSpec that is order-independent.
20
+ * This ensures {fg: 1, bg: 2} and {bg: 2, fg: 1} produce the same cache key.
21
+ */
22
+ function styleKey(spec: StyleSpec): string {
23
+ const parts: string[] = []
24
+ // Use fixed property order for deterministic keys
25
+ if (spec.fg !== undefined) {
26
+ parts.push(typeof spec.fg === "number" ? `f${spec.fg}` : `f${spec.fg.r},${spec.fg.g},${spec.fg.b}`)
27
+ }
28
+ if (spec.bg !== undefined) {
29
+ parts.push(typeof spec.bg === "number" ? `b${spec.bg}` : `b${spec.bg.r},${spec.bg.g},${spec.bg.b}`)
30
+ }
31
+ if (spec.bold) parts.push("B")
32
+ if (spec.italic) parts.push("I")
33
+ if (spec.underline) parts.push("U")
34
+ if (spec.inverse) parts.push("V")
35
+ return parts.join("|")
16
36
  }
17
37
 
18
38
  /**
@@ -20,56 +40,57 @@ export type StyleSpec = {
20
40
  * ANSI SGR escape strings. Id 0 always represents the default terminal style.
21
41
  */
22
42
  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>()
43
+ private nextId = 1 // 0 = default terminal style
44
+ private idByKey = new Map<string, number>()
45
+ private sgrById = new Map<number, string>()
26
46
 
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
- }
47
+ /** Get or create a numeric style id for a given style spec. */
48
+ id(spec?: StyleSpec): number {
49
+ if (!spec) return 0
50
+ const key = styleKey(spec)
51
+ if (key === "") return 0 // empty object = default style
52
+ let id = this.idByKey.get(key)
53
+ if (id) return id
54
+ id = this.nextId++
55
+ this.idByKey.set(key, id)
56
+ this.sgrById.set(id, this.buildSGR(spec))
57
+ return id
58
+ }
38
59
 
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
- }
60
+ /** Look up the ANSI SGR sequence for a style id. Id 0 resets attributes. */
61
+ sgr(id: number): string {
62
+ if (id === 0) return `${ESC}0m`
63
+ const s = this.sgrById.get(id)
64
+ return s ?? `${ESC}0m`
65
+ }
45
66
 
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")
67
+ /**
68
+ * Build an ANSI SGR sequence for a style spec.
69
+ * We always begin with reset (0) to avoid style drift across runs,
70
+ * then add only the requested attributes.
71
+ */
72
+ private buildSGR(spec: StyleSpec): string {
73
+ const parts: string[] = []
74
+ parts.push("0") // reset, then set exact attrs to avoid drift
75
+ if (spec.bold) parts.push("1")
76
+ if (spec.italic) parts.push("3")
77
+ if (spec.underline) parts.push("4")
78
+ if (spec.inverse) parts.push("7")
58
79
 
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
- }
80
+ const colorToCodes = (c: ColorValue | undefined, isBg: boolean): string | undefined => {
81
+ if (c == null) return undefined
82
+ if (typeof c === "number") return (isBg ? "48;5;" : "38;5;") + String(c | 0)
83
+ const clamp = (n: number) => (n < 0 ? 0 : n > 255 ? 255 : n | 0)
84
+ return `${isBg ? "48;2;" : "38;2;"}${clamp(c.r)};${clamp(c.g)};${clamp(c.b)}`
85
+ }
65
86
 
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)
87
+ const fgc = colorToCodes(spec.fg, false)
88
+ const bgc = colorToCodes(spec.bg, true)
89
+ if (fgc) parts.push(fgc)
90
+ if (bgc) parts.push(bgc)
70
91
 
71
- return `${CSI}${parts.join(";")}m`
72
- }
92
+ return `${CSI}${parts.join(";")}m`
93
+ }
73
94
  }
74
95
 
75
96
  /**
@@ -77,37 +98,37 @@ export class Palette {
77
98
  * requested style, while delegating id/sgr generation to a shared Palette.
78
99
  */
79
100
  export class ScopedPalette extends Palette {
80
- constructor(
81
- private readonly base: Palette,
82
- private readonly baseStyle?: StyleSpec,
83
- ) {
84
- super()
85
- }
101
+ constructor(
102
+ private readonly base: Palette,
103
+ private readonly baseStyle?: StyleSpec,
104
+ ) {
105
+ super()
106
+ }
86
107
 
87
- override id(spec?: StyleSpec): number {
88
- return this.base.id(mergeStyle(this.baseStyle, spec))
89
- }
108
+ override id(spec?: StyleSpec): number {
109
+ return this.base.id(mergeStyle(this.baseStyle, spec))
110
+ }
90
111
 
91
- override sgr(id: number): string {
92
- return this.base.sgr(id)
93
- }
112
+ override sgr(id: number): string {
113
+ return this.base.sgr(id)
114
+ }
94
115
  }
95
116
 
96
117
  /** Merge two style specs (b overrides a). Returns undefined if nothing to set. */
97
118
  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
119
+ if (!a && !b) return undefined
120
+ const merged: StyleSpec = {}
121
+ const fg = b?.fg ?? a?.fg
122
+ const bg = b?.bg ?? a?.bg
123
+ const bold = b?.bold ?? a?.bold
124
+ const italic = b?.italic ?? a?.italic
125
+ const underline = b?.underline ?? a?.underline
126
+ const inverse = b?.inverse ?? a?.inverse
127
+ if (fg !== undefined) merged.fg = fg
128
+ if (bg !== undefined) merged.bg = bg
129
+ if (bold !== undefined) merged.bold = bold
130
+ if (italic !== undefined) merged.italic = italic
131
+ if (underline !== undefined) merged.underline = underline
132
+ if (inverse !== undefined) merged.inverse = inverse
133
+ return Object.keys(merged).length > 0 ? merged : undefined
113
134
  }
@@ -0,0 +1,27 @@
1
+ /* segmenter.ts — Cached grapheme segmenter for text processing */
2
+
3
+ export type GraphemeSegmenter = {
4
+ segment(input: string): Iterable<{ segment: string }>
5
+ }
6
+
7
+ // Cached segmenter instance (created once, reused for all calls)
8
+ let cachedSegmenter: GraphemeSegmenter | null | undefined = undefined
9
+
10
+ /** Get or create a cached grapheme segmenter. Returns null if Intl.Segmenter unavailable. */
11
+ export function getGraphemeSegmenter(): GraphemeSegmenter | null {
12
+ if (cachedSegmenter !== undefined) return cachedSegmenter
13
+
14
+ const SegCtor: { new (...args: any[]): GraphemeSegmenter } | undefined = (globalThis as any).Intl?.Segmenter
15
+ if (typeof SegCtor === "function") {
16
+ try {
17
+ cachedSegmenter = new (SegCtor as new (...args: any[]) => GraphemeSegmenter)(undefined, {
18
+ granularity: "grapheme",
19
+ }) as GraphemeSegmenter
20
+ return cachedSegmenter
21
+ } catch {
22
+ // ignore and fall back
23
+ }
24
+ }
25
+ cachedSegmenter = null
26
+ return null
27
+ }
@@ -1,238 +1,152 @@
1
1
  import { CellBuffer, type Wcwidth, defaultWcwidth } from "./buffer.js"
2
2
  import { Palette, ScopedPalette, type ColorValue, type StyleSpec, mergeStyle } from "./palette.js"
3
+ import { Colors, parseColor, BASE_NAMES, type Color, type HexColor, type BaseColorName } from "../colors.js"
4
+ import { emitRow } from "../output.js"
3
5
 
4
6
  // Re-export palette types for other modules
5
7
  export type { ColorValue, StyleSpec }
6
8
  export { Palette, mergeStyle }
7
9
 
8
- /** Color inputs accepted by helpers: numeric, rgb object, or string (hex/name/grayN). */
9
- export type ColorLike = ColorValue | string
10
-
11
- export const BASE_NAMES = {
12
- black: 0,
13
- red: 1,
14
- green: 2,
15
- yellow: 3,
16
- blue: 4,
17
- magenta: 5,
18
- cyan: 6,
19
- white: 7,
20
- brightBlack: 8,
21
- brightRed: 9,
22
- brightGreen: 10,
23
- brightYellow: 11,
24
- brightBlue: 12,
25
- brightMagenta: 13,
26
- brightCyan: 14,
27
- brightWhite: 15,
28
- } as const
29
-
30
- /**
31
- * Parse flexible color inputs into a ColorValue.
32
- * Supports: number (0..255), {r,g,b}, "#rrggbb", base names, and gray0..gray23.
33
- */
34
- export function parseColor(c: ColorLike): ColorValue {
35
- if (typeof c === "number") return c
36
- if (typeof c === "object") return { r: c.r | 0, g: c.g | 0, b: c.b | 0 }
37
- const s = String(c).trim()
38
-
39
- if (s.startsWith("#")) {
40
- const hex = s.slice(1)
41
- if (hex.length === 6) {
42
- const r = parseInt(hex.slice(0, 2), 16)
43
- const g = parseInt(hex.slice(2, 4), 16)
44
- const b = parseInt(hex.slice(4, 6), 16)
45
- return { r, g, b }
46
- }
47
- }
48
-
49
- if (s in BASE_NAMES) return BASE_NAMES[s as keyof typeof BASE_NAMES]
50
-
51
- const m = /^(?:gray|grey)(\d{1,2})$/.exec(s)
52
- if (m?.[1]) {
53
- const n = Math.max(0, Math.min(23, parseInt(m[1], 10)))
54
- return 232 + n
55
- }
56
-
57
- return 7 // default to white if unrecognized
58
- }
59
-
60
- type ColorsApi = typeof BASE_NAMES & {
61
- rgb(r: number, g: number, b: number): ColorValue
62
- hex(hex: string): ColorValue
63
- gray(level: number): number
64
- }
65
-
66
- export const Colors: ColorsApi = Object.assign(
67
- {
68
- rgb(r: number, g: number, b: number): ColorValue {
69
- return { r, g, b }
70
- },
71
- hex(hex: string): ColorValue {
72
- return parseColor(hex)
73
- },
74
- gray(level: number): number {
75
- const n = Math.max(0, Math.min(23, level | 0))
76
- return 232 + n
77
- },
78
- },
79
- BASE_NAMES,
80
- ) as ColorsApi
10
+ // Re-export color utilities from colors.ts
11
+ export { Colors, parseColor, BASE_NAMES }
12
+ export type { Color, HexColor, BaseColorName }
81
13
 
82
14
  export function derivePalette(p: Palette, base?: StyleSpec): Palette {
83
- return new ScopedPalette(p, base)
15
+ return new ScopedPalette(p, base)
84
16
  }
85
17
 
86
18
  export class Surface {
87
- readonly w: number
88
- readonly h: number
89
- private B: CellBuffer
90
- private palette: Palette
91
-
92
- constructor(width: number, height: number, opts?: { palette?: Palette; wcwidth?: Wcwidth }) {
93
- this.w = Math.max(1, width | 0)
94
- this.h = Math.max(1, height | 0)
95
-
96
- const wc = opts?.wcwidth ?? defaultWcwidth
97
- this.B = new CellBuffer(this.w, this.h, wc)
98
- this.palette = opts?.palette ?? new Palette()
99
-
100
- this.B.clear(0)
101
- }
102
-
103
- /** Clear the back buffer (B) to spaces in the given style. */
104
- clear(styleId = 0): void {
105
- this.B.clear(styleId)
106
- }
107
-
108
- /** Draw a single code point at (x,y). If width=2, marks the following cell as continuation (w=0). */
109
- drawCP(x: number, y: number, cp: number, styleId = 0): void {
110
- this.B.drawCP(x, y, cp, styleId)
111
- }
112
-
113
- /** Draw plain text (no wrapping) clipped to width. Uses Intl.Segmenter if available. */
114
- drawText(x: number, y: number, text: string, styleId = 0, maxWidth?: number): void {
115
- this.B.drawText(x, y, text, styleId, maxWidth)
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
- this.B.fillRect(x, y, w, h, cp, styleId)
121
- }
122
-
123
- // --- Clipping helpers (forwarded to back buffer) ---
124
-
125
- pushClip(x: number, y: number, w: number, h: number): void {
126
- this.B.pushClip(x, y, w, h)
127
- }
128
-
129
- popClip(): void {
130
- this.B.popClip()
131
- }
132
-
133
- withClip(x: number, y: number, w: number, h: number, fn: () => void): void {
134
- this.B.withClip(x, y, w, h, fn)
135
- }
136
-
137
- // --- Offset helpers (forwarded to back buffer) ---
138
-
139
- pushOffset(dx: number, dy: number): void {
140
- this.B.pushOffset(dx, dy)
141
- }
142
-
143
- popOffset(): void {
144
- this.B.popOffset()
145
- }
146
-
147
- withOffset(dx: number, dy: number, fn: () => void): void {
148
- this.B.withOffset(dx, dy, fn)
149
- }
150
-
151
- /** Convert surface to plain text (for testing). */
152
- toString(): string {
153
- const lines: string[] = []
154
- for (let y = 0; y < this.h; y++) {
155
- const row = y * this.w
156
- let line = ""
157
- for (let x = 0; x < this.w; x++) {
158
- const i = row + x
159
- const cp = this.B.g[i]
160
- const w = this.B.cw[i]
161
- if (w !== 0) line += cp === 32 ? " " : String.fromCodePoint(cp)
162
- }
163
- // trim right spaces
164
- line = line.replace(/\s+$/, "")
165
- lines.push(line)
166
- }
167
- return lines.join("\n")
168
- }
169
-
170
- /** Compute tight bounding box of content in back buffer: any non-space glyph or non-default style. */
171
- contentBounds(): { x: number; y: number; w: number; h: number } {
172
- const W = this.w
173
- const H = this.h
174
- const g = this.B.g
175
- const s = this.B.s
176
- const cw = this.B.cw
177
- let minX = W,
178
- minY = H,
179
- maxX = -1,
180
- maxY = -1
181
- for (let y = 0; y < H; y++) {
182
- const row = y * W
183
- for (let x = 0; x < W; x++) {
184
- const i = row + x
185
- if (cw[i] === 0) continue
186
- if (g[i] !== 32 || s[i] !== 0) {
187
- if (x < minX) minX = x
188
- if (y < minY) minY = y
189
- if (x > maxX) maxX = x
190
- if (y > maxY) maxY = y
191
- }
192
- }
193
- }
194
- if (maxX < minX || maxY < minY) return { x: 0, y: 0, w: 0, h: 0 }
195
- return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 }
196
- }
197
-
198
- /** Build styled text lines for a given bounds region of the back buffer (without trailing newlines). */
199
- buildStyledLines(bounds: { x: number; y: number; w: number; h: number }, palette: Palette = this.palette): string[] {
200
- const W = this.w
201
- const g = this.B.g
202
- const s = this.B.s
203
- const cw = this.B.cw
204
- const lines: string[] = []
205
- const { x: bx, y: by, w: bw, h: bh } = bounds
206
- const endY = Math.min(by + bh, this.h)
207
- const endX = Math.min(bx + bw, W)
208
-
209
- for (let y = Math.max(0, by); y < endY; y++) {
210
- const row = y * W
211
- let line = ""
212
- let currentStyle = -1
213
-
214
- for (let x = Math.max(0, bx); x < endX; x++) {
215
- const i = row + x
216
- if (cw[i] === 0) continue // skip continuation cells
217
-
218
- const styleId = s[i]
219
- if (styleId !== currentStyle) {
220
- line += palette.sgr(styleId)
221
- currentStyle = styleId
222
- }
223
-
224
- const cp = g[i]
225
- line += cp === 32 ? " " : String.fromCodePoint(cp)
226
- }
227
-
228
- // reset style at end of line
229
- if (currentStyle !== 0) {
230
- line += palette.sgr(0)
231
- }
232
-
233
- lines.push(line)
234
- }
235
-
236
- return lines
237
- }
19
+ readonly w: number
20
+ readonly h: number
21
+ private B: CellBuffer
22
+ private palette: Palette
23
+
24
+ /** Access the underlying CellBuffer for diffing or inspection. */
25
+ get buffer(): CellBuffer {
26
+ return this.B
27
+ }
28
+
29
+ constructor(width: number, height: number, opts?: { palette?: Palette; wcwidth?: Wcwidth }) {
30
+ this.w = Math.max(1, width | 0)
31
+ this.h = Math.max(1, height | 0)
32
+
33
+ const wc = opts?.wcwidth ?? defaultWcwidth
34
+ this.B = new CellBuffer(this.w, this.h, wc)
35
+ this.palette = opts?.palette ?? new Palette()
36
+
37
+ this.B.clear(0)
38
+ }
39
+
40
+ /** Clear the back buffer (B) to spaces in the given style. */
41
+ clear(styleId = 0): void {
42
+ this.B.clear(styleId)
43
+ }
44
+
45
+ /** Draw a single code point at (x,y). If width=2, marks the following cell as continuation (w=0). */
46
+ drawCP(x: number, y: number, cp: number, styleId = 0): void {
47
+ this.B.drawCP(x, y, cp, styleId)
48
+ }
49
+
50
+ /** Draw plain text (no wrapping) clipped to width. Uses Intl.Segmenter if available. */
51
+ drawText(x: number, y: number, text: string, styleId = 0, maxWidth?: number): void {
52
+ this.B.drawText(x, y, text, styleId, maxWidth)
53
+ }
54
+
55
+ /** Fill a rectangle with a code point + style. */
56
+ fillRect(x: number, y: number, w: number, h: number, cp = 32, styleId = 0): void {
57
+ this.B.fillRect(x, y, w, h, cp, styleId)
58
+ }
59
+
60
+ // --- Clipping helpers (forwarded to back buffer) ---
61
+
62
+ pushClip(x: number, y: number, w: number, h: number): void {
63
+ this.B.pushClip(x, y, w, h)
64
+ }
65
+
66
+ popClip(): void {
67
+ this.B.popClip()
68
+ }
69
+
70
+ withClip(x: number, y: number, w: number, h: number, fn: () => void): void {
71
+ this.B.withClip(x, y, w, h, fn)
72
+ }
73
+
74
+ // --- Offset helpers (forwarded to back buffer) ---
75
+
76
+ pushOffset(dx: number, dy: number): void {
77
+ this.B.pushOffset(dx, dy)
78
+ }
79
+
80
+ popOffset(): void {
81
+ this.B.popOffset()
82
+ }
83
+
84
+ withOffset(dx: number, dy: number, fn: () => void): void {
85
+ this.B.withOffset(dx, dy, fn)
86
+ }
87
+
88
+ /** Convert surface to plain text (for testing). */
89
+ toString(): string {
90
+ const lines: string[] = []
91
+ for (let y = 0; y < this.h; y++) {
92
+ const row = y * this.w
93
+ let line = ""
94
+ for (let x = 0; x < this.w; x++) {
95
+ const i = row + x
96
+ const cp = this.B.g[i]
97
+ const w = this.B.cw[i]
98
+ if (w !== 0) line += cp === 32 ? " " : String.fromCodePoint(cp)
99
+ }
100
+ // trim right spaces
101
+ line = line.replace(/\s+$/, "")
102
+ lines.push(line)
103
+ }
104
+ return lines.join("\n")
105
+ }
106
+
107
+ /** Compute tight bounding box of content in back buffer: any non-space glyph or non-default style. */
108
+ contentBounds(): { x: number; y: number; w: number; h: number } {
109
+ const W = this.w
110
+ const H = this.h
111
+ const g = this.B.g
112
+ const s = this.B.s
113
+ const cw = this.B.cw
114
+ let minX = W,
115
+ minY = H,
116
+ maxX = -1,
117
+ maxY = -1
118
+ for (let y = 0; y < H; y++) {
119
+ const row = y * W
120
+ for (let x = 0; x < W; x++) {
121
+ const i = row + x
122
+ if (cw[i] === 0) continue
123
+ if (g[i] !== 32 || s[i] !== 0) {
124
+ if (x < minX) minX = x
125
+ if (y < minY) minY = y
126
+ if (x > maxX) maxX = x
127
+ if (y > maxY) maxY = y
128
+ }
129
+ }
130
+ }
131
+ if (maxX < minX || maxY < minY) return { x: 0, y: 0, w: 0, h: 0 }
132
+ return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 }
133
+ }
134
+
135
+ /** Build styled text lines for a given bounds region of the back buffer (without trailing newlines). */
136
+ buildStyledLines(bounds: { x: number; y: number; w: number; h: number }, palette: Palette = this.palette): string[] {
137
+ const W = this.w
138
+ const lines: string[] = []
139
+ const { x: bx, y: by, w: bw, h: bh } = bounds
140
+ const endY = Math.min(by + bh, this.h)
141
+ const endX = Math.min(bx + bw, W)
142
+ const startX = Math.max(0, bx)
143
+
144
+ for (let y = Math.max(0, by); y < endY; y++) {
145
+ const { output, lastStyle } = emitRow(this.B, palette, y, W, startX, endX)
146
+ const line = lastStyle !== 0 ? output + palette.sgr(0) : output
147
+ lines.push(line)
148
+ }
149
+
150
+ return lines
151
+ }
238
152
  }