@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
@@ -1,115 +0,0 @@
1
- // linearStack.ts — SwiftUI-like linear stacks with flexible children
2
-
3
- export type Axis = "horizontal" | "vertical"
4
- export type Size = { w: number; h: number }
5
- export type Rect = { x: number; y: number; w: number; h: number }
6
- export type HAlign = "leading" | "center" | "trailing"
7
- export type VAlign = "top" | "center" | "bottom"
8
-
9
- /** flexBase[i] = null for fixed, or the child's base main-size for flexible. */
10
- export function measureLinear(
11
- axis: Axis,
12
- maxW: number,
13
- maxH: number,
14
- spacing: number,
15
- sizes: Size[],
16
- flexBase: (number | null)[],
17
- ): Size {
18
- const n = sizes.length
19
- const gaps = Math.max(0, n - 1) * Math.max(0, spacing)
20
-
21
- let mainSum = 0
22
- let crossMax = 0
23
- let hasFlex = false
24
-
25
- for (let i = 0; i < n; i++) {
26
- const s = sizes[i]
27
- if (!s) continue
28
- const main = axis === "horizontal" ? s.w : s.h
29
- const cross = axis === "horizontal" ? s.h : s.w
30
- crossMax = Math.max(crossMax, cross)
31
- hasFlex ||= flexBase[i] != null
32
- mainSum += flexBase[i] ?? main
33
- }
34
-
35
- const maxMain = axis === "horizontal" ? maxW : maxH
36
- const totalMain = hasFlex ? maxMain : Math.min(maxMain, mainSum + gaps)
37
-
38
- return axis === "horizontal"
39
- ? { w: Math.min(maxW, totalMain), h: Math.min(maxH, crossMax) }
40
- : { w: Math.min(maxW, crossMax), h: Math.min(maxH, totalMain) }
41
- }
42
-
43
- export function layoutLinear(
44
- axis: Axis,
45
- rect: Rect,
46
- spacing: number,
47
- crossAlign: HAlign | VAlign,
48
- sizes: Size[],
49
- flexBase: (number | null)[],
50
- ): Rect[] {
51
- const n = sizes.length
52
- const out: Rect[] = new Array(n)
53
- const gaps = Math.max(0, n - 1) * Math.max(0, spacing)
54
-
55
- const mainAvail = axis === "horizontal" ? rect.w : rect.h
56
- const crossAvail = axis === "horizontal" ? rect.h : rect.w
57
-
58
- let baseSum = 0
59
- let flexCount = 0
60
- for (let i = 0; i < n; i++) {
61
- if (flexBase[i] != null) {
62
- baseSum += flexBase[i]!
63
- flexCount++
64
- } else {
65
- const size = sizes[i]
66
- if (size) {
67
- baseSum += axis === "horizontal" ? size.w : size.h
68
- }
69
- }
70
- }
71
-
72
- const leftover = Math.max(0, mainAvail - baseSum - gaps)
73
- const per = flexCount > 0 ? Math.floor(leftover / flexCount) : 0
74
- let rem = flexCount > 0 ? leftover - per * flexCount : 0
75
-
76
- const crossOffset = (childCross: number) => {
77
- if (crossAlign === "center") return Math.floor((crossAvail - childCross) / 2)
78
- if ((crossAlign === "trailing" && axis === "vertical") || (crossAlign === "bottom" && axis === "horizontal"))
79
- return Math.max(0, crossAvail - childCross)
80
- return 0
81
- }
82
-
83
- let pos = 0
84
- for (let i = 0; i < n; i++) {
85
- const s = sizes[i]
86
- if (!s) continue
87
- const baseMain = flexBase[i] ?? (axis === "horizontal" ? s.w : s.h)
88
- const flexExtra = flexBase[i] != null ? per + (rem > 0 ? (rem--, 1) : 0) : 0
89
- const wantMain = baseMain + flexExtra
90
- const childCross = Math.min(crossAvail, axis === "horizontal" ? s.h : s.w)
91
- const remaining = Math.max(0, mainAvail - pos)
92
- const takeMain = Math.min(wantMain, remaining)
93
- const cs = crossOffset(childCross)
94
-
95
- out[i] =
96
- axis === "horizontal"
97
- ? { x: rect.x + pos, y: rect.y + cs, w: Math.max(0, takeMain), h: Math.max(0, childCross) }
98
- : { x: rect.x + cs, y: rect.y + pos, w: Math.max(0, childCross), h: Math.max(0, takeMain) }
99
-
100
- pos += takeMain
101
- if (i < n - 1) pos += spacing
102
-
103
- if (pos >= mainAvail) {
104
- for (let j = i + 1; j < n; j++) {
105
- out[j] =
106
- axis === "horizontal"
107
- ? { x: rect.x + mainAvail, y: rect.y, w: 0, h: 0 }
108
- : { x: rect.x, y: rect.y + mainAvail, w: 0, h: 0 }
109
- }
110
- break
111
- }
112
- }
113
-
114
- return out
115
- }
@@ -1,335 +0,0 @@
1
- // motion-value.ts - Generic MotionValue<T> with adapters and animate()
2
-
3
- import type { ColorLike, ColorValue } from "./render/surface.js"
4
- import { parseColor } from "./render/surface.js"
5
- import { idxToRGB, clamp255 } from "./render/color-utils.js"
6
- import { DEFAULTS, fromDurationBounce, stepSpring1D, type SpringConfig } from "./spring-physics.js"
7
- import { colorLikeToRGB } from "./spring.js"
8
-
9
- // ---- Types ----
10
-
11
- export type SpringOptions = SpringConfig & { maxDt?: number }
12
- export type Unsubscribe = () => void
13
-
14
- export interface AnimationControls {
15
- readonly time: number
16
- readonly speed: number
17
- isRunning(): boolean
18
- play(): void
19
- pause(): void
20
- stop(): void // complete & settle
21
- cancel(): void // cancel and keep current value
22
- seek(t: number): void
23
- finished: Promise<void>
24
- }
25
-
26
- export interface Adapter<T> {
27
- toArray(v: T): number[]
28
- fromArray(a: ReadonlyArray<number>): T
29
- clamp?(a: number[]): number[]
30
- equals?(a: T, b: T): boolean
31
- }
32
-
33
- export interface MotionValue<T> {
34
- get(): T
35
- set(next: T): void
36
- setTarget(next: T): void
37
- getVelocity(): T
38
- onChange(fn: (v: T) => void): Unsubscribe
39
- onSettle(fn: () => void): Unsubscribe
40
- animate(to: T, options?: SpringOptions): AnimationControls
41
- stop(): void
42
- destroy(): void
43
- }
44
-
45
- export interface ColorMotionValue extends MotionValue<ColorValue> {
46
- animate(to: ColorLike | ColorValue, options?: SpringOptions): AnimationControls
47
- }
48
-
49
- // ---- Built-in adapters ----
50
-
51
- export const numberAdapter: Adapter<number> = {
52
- toArray: (n) => [n],
53
- fromArray: (a) => a[0] ?? 0,
54
- clamp: (a) => a,
55
- equals: (a, b) => Object.is(a, b),
56
- }
57
-
58
- export const rgbAdapter: Adapter<ColorValue> = {
59
- toArray: (c) => [
60
- typeof c === "number" ? idxToRGB(c).r : c.r,
61
- typeof c === "number" ? idxToRGB(c).g : c.g,
62
- typeof c === "number" ? idxToRGB(c).b : c.b,
63
- ],
64
- fromArray: (a) => ({ r: clamp255(a[0] ?? 0), g: clamp255(a[1] ?? 0), b: clamp255(a[2] ?? 0) }),
65
- clamp: (a) => [clamp255(a[0] ?? 0), clamp255(a[1] ?? 0), clamp255(a[2] ?? 0)],
66
- equals: (a, b) =>
67
- typeof a === "number" || typeof b === "number"
68
- ? JSON.stringify(colorLikeToRGB(a as any)) === JSON.stringify(colorLikeToRGB(b as any))
69
- : a.r === b.r && a.g === b.g && a.b === b.b,
70
- }
71
-
72
- // ---- Adapter registry ----
73
-
74
- type AdapterEntry = {
75
- test: (value: unknown) => boolean
76
- create: (value: any) => Adapter<any>
77
- normalize?: (value: any) => any
78
- }
79
-
80
- const ADAPTERS: AdapterEntry[] = []
81
-
82
- export function registerAdapter(entry: AdapterEntry) {
83
- ADAPTERS.unshift(entry)
84
- }
85
-
86
- function isColorObject(x: any): x is { r: number; g: number; b: number } {
87
- return x && typeof x === "object" && typeof x.r === "number" && typeof x.g === "number" && typeof x.b === "number"
88
- }
89
-
90
- function isColorLike(v: unknown): v is ColorLike {
91
- if (isColorObject(v)) return true
92
- if (typeof v === "string" || typeof v === "number") {
93
- const parsed = parseColor(v as any)
94
- return typeof parsed === "number" || isColorObject(parsed)
95
- }
96
- return false
97
- }
98
-
99
- // Register built-in adapters
100
- registerAdapter({
101
- test: (v) => isColorLike(v),
102
- create: () => rgbAdapter,
103
- normalize: (v) => colorLikeToRGB(v as ColorLike),
104
- })
105
- registerAdapter({
106
- test: (v) => typeof v === "number",
107
- create: () => numberAdapter,
108
- })
109
-
110
- // ---- Frameloop (Node-friendly setTimeout-based) ----
111
-
112
- type Tickable = () => boolean // keep ticking?
113
- const tickers = new Set<Tickable>()
114
- let timer: NodeJS.Timeout | null = null
115
-
116
- function pump() {
117
- const copy = Array.from(tickers)
118
- for (const t of copy) {
119
- const keep = t()
120
- if (!keep) tickers.delete(t)
121
- }
122
- if (tickers.size === 0 && timer) {
123
- clearTimeout(timer)
124
- timer = null
125
- } else {
126
- // aim ~60fps
127
- timer = setTimeout(pump, 16)
128
- }
129
- }
130
-
131
- function ensureLoop() {
132
- if (!timer) timer = setTimeout(pump, 16)
133
- }
134
-
135
- // ---- AnimationControls implementation ----
136
-
137
- class AnimationControlsImpl implements AnimationControls {
138
- private _running = false
139
- private _resolve!: () => void
140
- finished: Promise<void> = new Promise<void>((res) => (this._resolve = res))
141
- time = 0
142
- speed = 1
143
- isRunning(): boolean {
144
- return this._running
145
- }
146
- play(): void {
147
- if (!this._running) {
148
- this._running = true
149
- }
150
- }
151
- pause(): void {
152
- this._running = false
153
- }
154
- stop(): void {
155
- this._running = false
156
- this._resolve()
157
- }
158
- cancel(): void {
159
- this._running = false
160
- }
161
- seek(t: number): void {
162
- this.time = Math.max(0, t)
163
- }
164
- }
165
-
166
- // ---- MotionValue implementation ----
167
-
168
- class MotionValueImpl<T> implements MotionValue<T> {
169
- private adapter: Adapter<T>
170
- private opts: Required<SpringOptions>
171
- private x: number[]
172
- private v: number[]
173
- private target: number[]
174
- private last: number | undefined
175
- private changeSubs = new Set<(v: T) => void>()
176
- private settleSubs = new Set<() => void>()
177
- private controls?: AnimationControlsImpl
178
-
179
- constructor(initial: T, adapter: Adapter<T>, opts?: SpringOptions) {
180
- this.adapter = adapter
181
- const base = { ...DEFAULTS, maxDt: 0.1, ...(opts ?? {}) }
182
- this.opts = base as Required<SpringOptions>
183
- if (this.opts.duration != null) {
184
- const derived = fromDurationBounce(this.opts.duration, this.opts.bounce ?? DEFAULTS.bounce)
185
- this.opts.frequency = derived.frequency
186
- this.opts.dampingRatio = derived.dampingRatio
187
- }
188
- this.x = adapter.toArray(initial)
189
- this.v = this.x.map(() => 0)
190
- this.target = this.x.slice()
191
- }
192
-
193
- get(): T {
194
- return this.adapter.fromArray(this.x)
195
- }
196
- set(next: T): void {
197
- this.x = this.adapter.toArray(next)
198
- if (this.adapter.clamp) this.x = this.adapter.clamp(this.x)
199
- this.v = this.v.map(() => 0)
200
- this.target = this.x.slice()
201
- this.emitChange()
202
- }
203
- setTarget(next: T): void {
204
- this.target = this.adapter.toArray(next)
205
- if (this.adapter.clamp) this.target = this.adapter.clamp(this.target)
206
- }
207
- getVelocity(): T {
208
- return this.adapter.fromArray(this.v)
209
- }
210
- onChange(fn: (v: T) => void): Unsubscribe {
211
- this.changeSubs.add(fn)
212
- return () => this.changeSubs.delete(fn)
213
- }
214
- onSettle(fn: () => void): Unsubscribe {
215
- this.settleSubs.add(fn)
216
- return () => this.settleSubs.delete(fn)
217
- }
218
-
219
- animate(to: T, options?: SpringOptions): AnimationControls {
220
- this.setTarget(to)
221
- this.opts = { ...this.opts, ...(options ?? {}) } as Required<SpringOptions>
222
- if (this.opts.duration != null) {
223
- const derived = fromDurationBounce(this.opts.duration, this.opts.bounce ?? DEFAULTS.bounce)
224
- this.opts.frequency = derived.frequency
225
- this.opts.dampingRatio = derived.dampingRatio
226
- }
227
- this.controls?.cancel()
228
- const controls = new AnimationControlsImpl()
229
- controls.play()
230
- this.controls = controls
231
- this.last = undefined
232
-
233
- const tick = () => {
234
- if (!controls.isRunning()) return false
235
- const now = Date.now()
236
- const last = this.last ?? now
237
- const dt = Math.max(0, Math.min(this.opts.maxDt ?? 0.1, (now - last) / 1000))
238
- this.last = now
239
- const { frequency: f, dampingRatio: z, epsilon, bounce } = this.opts
240
- let settled = true
241
- for (let i = 0; i < this.x.length; i++) {
242
- const prevX = this.x[i]
243
- let { x: nextX, v: nextV } = stepSpring1D(prevX, this.v[i], this.target[i], dt, f!, z!)
244
- if ((bounce ?? DEFAULTS.bounce) <= 0) {
245
- const crossed = (prevX - this.target[i]) * (nextX - this.target[i]) <= 0
246
- if (crossed) {
247
- nextX = this.target[i]
248
- nextV = 0
249
- }
250
- }
251
- this.x[i] = nextX
252
- this.v[i] = nextV
253
- if (
254
- !(
255
- Math.abs(this.x[i] - this.target[i]) < (epsilon ?? DEFAULTS.epsilon!) &&
256
- Math.abs(this.v[i]) < (epsilon ?? DEFAULTS.epsilon!)
257
- )
258
- ) {
259
- settled = false
260
- }
261
- }
262
- if (this.adapter.clamp) this.x = this.adapter.clamp(this.x)
263
- this.emitChange()
264
- if (settled) {
265
- this.x = this.target.slice()
266
- this.v = this.v.map(() => 0)
267
- this.emitChange()
268
- this.emitSettle()
269
- controls.stop()
270
- return false
271
- }
272
- return true
273
- }
274
- tickers.add(tick)
275
- ensureLoop()
276
- return controls
277
- }
278
-
279
- stop(): void {
280
- this.controls?.stop()
281
- }
282
- destroy(): void {
283
- this.changeSubs.clear()
284
- this.settleSubs.clear()
285
- this.controls?.cancel()
286
- }
287
-
288
- private emitChange() {
289
- const v = this.get()
290
- this.changeSubs.forEach((fn) => fn(v))
291
- }
292
- private emitSettle() {
293
- this.settleSubs.forEach((fn) => fn())
294
- }
295
- }
296
-
297
- class ColorMotionValueImpl extends MotionValueImpl<ColorValue> implements ColorMotionValue {
298
- override animate(to: ColorLike | ColorValue, options?: SpringOptions): AnimationControls {
299
- const target =
300
- typeof to === "string" || typeof to === "number" ? colorLikeToRGB(to as ColorLike) : (to as ColorValue)
301
- return super.animate(target, options)
302
- }
303
- }
304
-
305
- // ---- Factory ----
306
-
307
- export function motionValue(initial: number, opts?: SpringOptions & { adapter?: Adapter<number> }): MotionValue<number>
308
- export function motionValue(
309
- initial: ColorLike,
310
- opts?: SpringOptions & { adapter?: Adapter<ColorValue> },
311
- ): ColorMotionValue
312
- export function motionValue<T>(initial: T, opts?: SpringOptions & { adapter?: Adapter<T> }): MotionValue<T>
313
- export function motionValue(initial: any, opts?: SpringOptions & { adapter?: Adapter<any> }): MotionValue<any> {
314
- if (opts?.adapter) {
315
- const norm = opts.adapter.fromArray(opts.adapter.toArray(initial))
316
- return new MotionValueImpl(norm, opts.adapter, opts)
317
- }
318
- for (const entry of ADAPTERS) {
319
- if (entry.test(initial)) {
320
- const adapter = entry.create(initial)
321
- const normalized = entry.normalize ? entry.normalize(initial) : adapter.fromArray(adapter.toArray(initial))
322
- if (adapter === rgbAdapter) return new ColorMotionValueImpl(normalized as ColorValue, adapter, opts)
323
- return new MotionValueImpl(normalized, adapter, opts)
324
- }
325
- }
326
- throw new Error("No adapter found for initial value. Provide { adapter } explicitly.")
327
- }
328
-
329
- // ---- Top-level animate helper ----
330
-
331
- export function animate<T>(mv: MotionValue<T>, to: T, opts?: SpringOptions): AnimationControls
332
- export function animate(mv: ColorMotionValue, to: ColorLike | ColorValue, opts?: SpringOptions): AnimationControls
333
- export function animate(mv: any, to: any, opts?: SpringOptions): AnimationControls {
334
- return mv.animate(to as any, opts)
335
- }
@@ -1,206 +0,0 @@
1
- import { Palette, Surface, type Surface as SurfaceType } from "../render/surface.js"
2
- import { diffFrames, type RunWriter } from "../render/diff.js"
3
- import { FullscreenWriter } from "./writers/fullscreen.js"
4
- import { LineDiffWriter } from "./writers/inline.js"
5
- import type { View } from "../ui/core/view.js"
6
- import { ANSI, Terminal } from "../ansi.js"
7
- import type { TerminalBackend } from "../runtime/backend_node.js"
8
- import { Effect } from "effect"
9
- import { geometryStore } from "../ui/core/geometry-store.js"
10
-
11
- export interface DisplayRenderer {
12
- start(): void
13
- draw(view: View): void
14
- resize(cols: number, rows: number): void
15
- end(): void
16
- }
17
-
18
- abstract class BaseRenderer implements DisplayRenderer {
19
- protected palette = new Palette()
20
- protected prev?: SurfaceType
21
- protected next?: SurfaceType
22
-
23
- constructor(protected backend: TerminalBackend) {}
24
- abstract start(): void
25
- abstract end(): void
26
- abstract makeWriter(): RunWriter
27
-
28
- protected ensureFrames(cols: number, rows: number) {
29
- const needNew =
30
- !this.prev ||
31
- !this.next ||
32
- this.prev.w !== cols ||
33
- this.prev.h !== rows ||
34
- this.next.w !== cols ||
35
- this.next.h !== rows
36
- if (needNew) {
37
- this.prev = new Surface(cols, rows, { palette: this.palette })
38
- this.next = new Surface(cols, rows, { palette: this.palette })
39
- this.prev.clear(0)
40
- this.next.clear(0)
41
- }
42
- }
43
-
44
- protected swap() {
45
- const t = this.prev!
46
- this.prev = this.next!
47
- this.next = t
48
- }
49
-
50
- draw(view: View): void {
51
- const { cols, rows } = this.backend.size()
52
- this.ensureFrames(cols, rows)
53
-
54
- // render into the "next" frame
55
- this.next?.clear(0)
56
- geometryStore.beginFrame()
57
- view.render(this.next!, this.palette, { x: 0, y: 0, w: cols, h: rows })
58
- geometryStore.endFrame()
59
-
60
- const writer = this.makeWriter()
61
- diffFrames(this.prev!, this.next!, writer)
62
- this.backend.write(writer.flush())
63
-
64
- // swap for next frame
65
- this.swap()
66
- }
67
-
68
- resize(_c: number, _r: number): void {
69
- // Clear terminal and force full rerender
70
- this.backend.write(Terminal.clearAndHome) // clear screen + move to top-left
71
- delete (this as any).prev
72
- delete (this as any).next
73
- }
74
- }
75
-
76
- export class FullscreenRenderer extends BaseRenderer {
77
- /**
78
- * Create a fullscreen renderer. Kept as an Effect to align with inline
79
- * and allow future async setup without changing call sites.
80
- */
81
- static make(backend: TerminalBackend) {
82
- return Effect.sync(() => new FullscreenRenderer(backend))
83
- }
84
- start(): void {
85
- this.backend.enterFullscreen()
86
- }
87
- end(): void {
88
- this.backend.exitFullscreen()
89
- }
90
- makeWriter(): RunWriter {
91
- return new FullscreenWriter(this.palette)
92
- }
93
- }
94
-
95
- export class InlineRenderer extends BaseRenderer {
96
- private writer = new LineDiffWriter(this.palette)
97
- private baseRow: number | undefined
98
- private skipDiff = false
99
- private lastPrintedHeight = 1
100
-
101
- /**
102
- * Create an inline renderer anchored to the current cursor row.
103
- */
104
- static make(backend: TerminalBackend) {
105
- return Effect.promise(() => backend.getCursorPosition()).pipe(
106
- Effect.catchAll(() => Effect.succeed({ row: process.stdout.rows || 24, col: 1 })),
107
- Effect.map((pos) => {
108
- const r = new InlineRenderer(backend)
109
- r.setBaseRow(Math.max(0, (pos.row | 0) - 1))
110
- return r
111
- }),
112
- )
113
- }
114
-
115
- setBaseRow(n: number) {
116
- this.baseRow = Math.max(0, n | 0)
117
- }
118
-
119
- start(): void {
120
- this.backend.hideCursor()
121
- }
122
- end(): void {
123
- const topRow = this.baseRow ?? 0
124
- const nextRow1 = Math.max(1, topRow + this.lastPrintedHeight)
125
- this.backend.write(ANSI.cursor.to(nextRow1 + 1, 1))
126
- this.backend.showCursor()
127
- }
128
- makeWriter(): RunWriter {
129
- return this.writer
130
- }
131
-
132
- // Override resize to clear screen and reset base row for inline mode
133
- override resize(c: number, r: number): void {
134
- // Clear entire screen, scrollback buffer, and move cursor to top-left
135
- this.backend.write(ANSI.screen.clearScrollback + Terminal.clearAndHome)
136
- // Reset base row to start from top
137
- this.baseRow = 0
138
- // Force full redraw on next draw
139
- this.skipDiff = true
140
- // Call parent to clear surfaces
141
- super.resize(c, r)
142
- }
143
-
144
- // Override draw to support inline base-row anchoring and scroll-up
145
- override draw(view: View): void {
146
- const { cols, rows } = this.backend.size()
147
- this.ensureFrames(cols, rows)
148
-
149
- let baseRow = this.baseRow ?? 0
150
-
151
- const measured = view.measure(cols, rows)
152
- const renderW = Math.max(1, Math.min(cols, measured.w || 0))
153
- const renderH = Math.max(1, Math.min(rows, measured.h || 0))
154
-
155
- // Render into the "next" frame (clamp to measured size so we don't fill the viewport)
156
- this.next?.clear(0)
157
- geometryStore.beginFrame()
158
- view.render(this.next!, this.palette, { x: 0, y: 0, w: renderW, h: renderH })
159
- geometryStore.endFrame()
160
-
161
- // Determine content footprint
162
- const bounds = this.next?.contentBounds()
163
- const contentH = Math.max(1, (bounds?.h ?? 0) | 0)
164
- this.lastPrintedHeight = contentH
165
-
166
- // If bottom would overflow viewport, scroll up and reduce baseRow
167
- const bottom1 = baseRow + contentH
168
- if (bottom1 > rows) {
169
- const scroll = bottom1 - rows
170
- if (scroll > 0) {
171
- this.backend.write(ANSI.scroll.up(scroll))
172
- baseRow = Math.max(0, baseRow - scroll)
173
- }
174
- }
175
-
176
- // Inform writer of base row for absolute addressing
177
- this.writer.setBaseRow?.(baseRow)
178
-
179
- // If we need to skip diff, render directly
180
- if (this.skipDiff) {
181
- // Position cursor at base row
182
- this.backend.write(ANSI.cursor.to(baseRow + 1, 1))
183
- // Just render what we have in the surface (viewport content)
184
- const output = this.next?.buildStyledLines({ x: 0, y: 0, w: this.next?.w ?? 0, h: contentH }, this.palette)
185
- this.backend.write((output ?? []).join("\n"))
186
- this.skipDiff = false
187
- } else {
188
- // Normal diff and flush
189
- diffFrames(this.prev!, this.next!, this.writer)
190
- this.backend.write(this.writer.flush())
191
- }
192
-
193
- // swap for next frame and advance baseline for subsequent renders
194
- this.swap()
195
- this.baseRow = baseRow
196
- }
197
- }
198
-
199
- export type RendererMode = "fullscreen" | "inline"
200
-
201
- export class TerminalRenderer {
202
- private constructor() {}
203
- static make(mode: RendererMode, backend: TerminalBackend) {
204
- return mode === "inline" ? InlineRenderer.make(backend) : FullscreenRenderer.make(backend)
205
- }
206
- }
@@ -1,58 +0,0 @@
1
- import type { RunWriter } from "../../render/diff.js"
2
- import type { Palette } from "../../render/surface.js"
3
- import { displayWidth } from "../../render/measure.js"
4
- import { ANSI } from "../../ansi.js"
5
-
6
- class ChunkBuilder {
7
- private parts: string[] = []
8
- push(s: string) {
9
- if (s) this.parts.push(s)
10
- }
11
- concat() {
12
- const out = this.parts.join("")
13
- this.parts = []
14
- return out
15
- }
16
- }
17
-
18
- export class FullscreenWriter implements RunWriter {
19
- private out = new ChunkBuilder()
20
- private curRow = -1
21
- private curCol = -1
22
- private curStyle = -1
23
- private rows = 0
24
-
25
- constructor(private palette: Palette) {}
26
-
27
- begin(vp: { cols: number; rows: number }) {
28
- this.rows = vp.rows
29
- }
30
-
31
- run(row: number, col: number, styleId: number, text: string): void {
32
- if (row !== this.curRow || col !== this.curCol) {
33
- this.out.push(ANSI.cursor.to(row + 1, col + 1))
34
- this.curRow = row
35
- this.curCol = col
36
- }
37
- if (this.curStyle !== styleId) {
38
- this.out.push(this.palette.sgr(styleId))
39
- this.curStyle = styleId
40
- }
41
- this.out.push(text)
42
- this.curCol += displayWidth(text) // fix for wide glyphs
43
- }
44
-
45
- end(): void {
46
- if (this.curStyle !== -1) this.out.push(ANSI.reset)
47
- // park cursor bottom-left (optional)
48
- this.out.push(ANSI.cursor.to(this.rows, 1))
49
- }
50
-
51
- flush(): string {
52
- const s = this.out.concat()
53
- // reset tracked cursor/style to unknown so next frame emits properly
54
- this.curRow = this.curCol = -1
55
- this.curStyle = -1
56
- return s
57
- }
58
- }