@effect-tui/react 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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/dist/jsx-dev-runtime.d.ts +3 -0
  4. package/dist/jsx-dev-runtime.d.ts.map +1 -0
  5. package/dist/jsx-dev-runtime.js +3 -0
  6. package/dist/jsx-dev-runtime.js.map +1 -0
  7. package/dist/jsx-runtime.d.ts +47 -0
  8. package/dist/jsx-runtime.d.ts.map +1 -0
  9. package/dist/jsx-runtime.js +6 -0
  10. package/dist/jsx-runtime.js.map +1 -0
  11. package/dist/src/codeblock.d.ts +9 -0
  12. package/dist/src/codeblock.d.ts.map +1 -0
  13. package/dist/src/codeblock.js +24 -0
  14. package/dist/src/codeblock.js.map +1 -0
  15. package/dist/src/constants.d.ts +3 -0
  16. package/dist/src/constants.d.ts.map +1 -0
  17. package/dist/src/constants.js +3 -0
  18. package/dist/src/constants.js.map +1 -0
  19. package/dist/src/debug/DiagnosticsPanel.d.ts +7 -0
  20. package/dist/src/debug/DiagnosticsPanel.d.ts.map +1 -0
  21. package/dist/src/debug/DiagnosticsPanel.js +13 -0
  22. package/dist/src/debug/DiagnosticsPanel.js.map +1 -0
  23. package/dist/src/highlight.d.ts +20 -0
  24. package/dist/src/highlight.d.ts.map +1 -0
  25. package/dist/src/highlight.js +51 -0
  26. package/dist/src/highlight.js.map +1 -0
  27. package/dist/src/hooks/index.d.ts +4 -0
  28. package/dist/src/hooks/index.d.ts.map +1 -0
  29. package/dist/src/hooks/index.js +3 -0
  30. package/dist/src/hooks/index.js.map +1 -0
  31. package/dist/src/hooks/use-keyboard.d.ts +18 -0
  32. package/dist/src/hooks/use-keyboard.d.ts.map +1 -0
  33. package/dist/src/hooks/use-keyboard.js +26 -0
  34. package/dist/src/hooks/use-keyboard.js.map +1 -0
  35. package/dist/src/hooks/use-paste.d.ts +5 -0
  36. package/dist/src/hooks/use-paste.d.ts.map +1 -0
  37. package/dist/src/hooks/use-paste.js +14 -0
  38. package/dist/src/hooks/use-paste.js.map +1 -0
  39. package/dist/src/hooks/useFrameStats.d.ts +7 -0
  40. package/dist/src/hooks/useFrameStats.d.ts.map +1 -0
  41. package/dist/src/hooks/useFrameStats.js +28 -0
  42. package/dist/src/hooks/useFrameStats.js.map +1 -0
  43. package/dist/src/hosts/base.d.ts +22 -0
  44. package/dist/src/hosts/base.d.ts.map +1 -0
  45. package/dist/src/hosts/base.js +53 -0
  46. package/dist/src/hosts/base.js.map +1 -0
  47. package/dist/src/hosts/box.d.ts +26 -0
  48. package/dist/src/hosts/box.d.ts.map +1 -0
  49. package/dist/src/hosts/box.js +84 -0
  50. package/dist/src/hosts/box.js.map +1 -0
  51. package/dist/src/hosts/canvas.d.ts +48 -0
  52. package/dist/src/hosts/canvas.d.ts.map +1 -0
  53. package/dist/src/hosts/canvas.js +109 -0
  54. package/dist/src/hosts/canvas.js.map +1 -0
  55. package/dist/src/hosts/codeblock.d.ts +32 -0
  56. package/dist/src/hosts/codeblock.d.ts.map +1 -0
  57. package/dist/src/hosts/codeblock.js +118 -0
  58. package/dist/src/hosts/codeblock.js.map +1 -0
  59. package/dist/src/hosts/hstack.d.ts +18 -0
  60. package/dist/src/hosts/hstack.d.ts.map +1 -0
  61. package/dist/src/hosts/hstack.js +45 -0
  62. package/dist/src/hosts/hstack.js.map +1 -0
  63. package/dist/src/hosts/index.d.ts +16 -0
  64. package/dist/src/hosts/index.d.ts.map +1 -0
  65. package/dist/src/hosts/index.js +40 -0
  66. package/dist/src/hosts/index.js.map +1 -0
  67. package/dist/src/hosts/spacer.d.ts +19 -0
  68. package/dist/src/hosts/spacer.d.ts.map +1 -0
  69. package/dist/src/hosts/spacer.js +28 -0
  70. package/dist/src/hosts/spacer.js.map +1 -0
  71. package/dist/src/hosts/text.d.ts +43 -0
  72. package/dist/src/hosts/text.d.ts.map +1 -0
  73. package/dist/src/hosts/text.js +148 -0
  74. package/dist/src/hosts/text.js.map +1 -0
  75. package/dist/src/hosts/vstack.d.ts +18 -0
  76. package/dist/src/hosts/vstack.d.ts.map +1 -0
  77. package/dist/src/hosts/vstack.js +45 -0
  78. package/dist/src/hosts/vstack.js.map +1 -0
  79. package/dist/src/hosts/zstack.d.ts +20 -0
  80. package/dist/src/hosts/zstack.d.ts.map +1 -0
  81. package/dist/src/hosts/zstack.js +65 -0
  82. package/dist/src/hosts/zstack.js.map +1 -0
  83. package/dist/src/index.d.ts +20 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +20 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/inline/index.d.ts +32 -0
  88. package/dist/src/inline/index.d.ts.map +1 -0
  89. package/dist/src/inline/index.js +111 -0
  90. package/dist/src/inline/index.js.map +1 -0
  91. package/dist/src/jsx.d.ts +2 -0
  92. package/dist/src/jsx.d.ts.map +1 -0
  93. package/dist/src/jsx.js +4 -0
  94. package/dist/src/jsx.js.map +1 -0
  95. package/dist/src/motion/color-motion-value.d.ts +32 -0
  96. package/dist/src/motion/color-motion-value.d.ts.map +1 -0
  97. package/dist/src/motion/color-motion-value.js +80 -0
  98. package/dist/src/motion/color-motion-value.js.map +1 -0
  99. package/dist/src/motion/color.d.ts +30 -0
  100. package/dist/src/motion/color.d.ts.map +1 -0
  101. package/dist/src/motion/color.js +172 -0
  102. package/dist/src/motion/color.js.map +1 -0
  103. package/dist/src/motion/color.test.d.ts +2 -0
  104. package/dist/src/motion/color.test.d.ts.map +1 -0
  105. package/dist/src/motion/color.test.js +97 -0
  106. package/dist/src/motion/color.test.js.map +1 -0
  107. package/dist/src/motion/event-emitter.d.ts +18 -0
  108. package/dist/src/motion/event-emitter.d.ts.map +1 -0
  109. package/dist/src/motion/event-emitter.js +30 -0
  110. package/dist/src/motion/event-emitter.js.map +1 -0
  111. package/dist/src/motion/frame.d.ts +9 -0
  112. package/dist/src/motion/frame.d.ts.map +1 -0
  113. package/dist/src/motion/frame.js +51 -0
  114. package/dist/src/motion/frame.js.map +1 -0
  115. package/dist/src/motion/hooks.d.ts +75 -0
  116. package/dist/src/motion/hooks.d.ts.map +1 -0
  117. package/dist/src/motion/hooks.js +190 -0
  118. package/dist/src/motion/hooks.js.map +1 -0
  119. package/dist/src/motion/index.d.ts +4 -0
  120. package/dist/src/motion/index.d.ts.map +1 -0
  121. package/dist/src/motion/index.js +7 -0
  122. package/dist/src/motion/index.js.map +1 -0
  123. package/dist/src/motion/motion-value.d.ts +40 -0
  124. package/dist/src/motion/motion-value.d.ts.map +1 -0
  125. package/dist/src/motion/motion-value.js +109 -0
  126. package/dist/src/motion/motion-value.js.map +1 -0
  127. package/dist/src/motion/motion-value.test.d.ts +2 -0
  128. package/dist/src/motion/motion-value.test.d.ts.map +1 -0
  129. package/dist/src/motion/motion-value.test.js +177 -0
  130. package/dist/src/motion/motion-value.test.js.map +1 -0
  131. package/dist/src/motion/spring-math.d.ts +28 -0
  132. package/dist/src/motion/spring-math.d.ts.map +1 -0
  133. package/dist/src/motion/spring-math.js +81 -0
  134. package/dist/src/motion/spring-math.js.map +1 -0
  135. package/dist/src/motion/types.d.ts +25 -0
  136. package/dist/src/motion/types.d.ts.map +1 -0
  137. package/dist/src/motion/types.js +13 -0
  138. package/dist/src/motion/types.js.map +1 -0
  139. package/dist/src/output.d.ts +47 -0
  140. package/dist/src/output.d.ts.map +1 -0
  141. package/dist/src/output.js +125 -0
  142. package/dist/src/output.js.map +1 -0
  143. package/dist/src/profiler.d.ts +6 -0
  144. package/dist/src/profiler.d.ts.map +1 -0
  145. package/dist/src/profiler.js +73 -0
  146. package/dist/src/profiler.js.map +1 -0
  147. package/dist/src/reconciler/host-config.d.ts +16 -0
  148. package/dist/src/reconciler/host-config.d.ts.map +1 -0
  149. package/dist/src/reconciler/host-config.js +174 -0
  150. package/dist/src/reconciler/host-config.js.map +1 -0
  151. package/dist/src/reconciler/types.d.ts +52 -0
  152. package/dist/src/reconciler/types.d.ts.map +1 -0
  153. package/dist/src/reconciler/types.js +2 -0
  154. package/dist/src/reconciler/types.js.map +1 -0
  155. package/dist/src/renderer.d.ts +101 -0
  156. package/dist/src/renderer.d.ts.map +1 -0
  157. package/dist/src/renderer.js +509 -0
  158. package/dist/src/renderer.js.map +1 -0
  159. package/dist/src/terminal.d.ts +37 -0
  160. package/dist/src/terminal.d.ts.map +1 -0
  161. package/dist/src/terminal.js +65 -0
  162. package/dist/src/terminal.js.map +1 -0
  163. package/dist/src/test/index.d.ts +3 -0
  164. package/dist/src/test/index.d.ts.map +1 -0
  165. package/dist/src/test/index.js +3 -0
  166. package/dist/src/test/index.js.map +1 -0
  167. package/dist/src/test/mock-streams.d.ts +44 -0
  168. package/dist/src/test/mock-streams.d.ts.map +1 -0
  169. package/dist/src/test/mock-streams.js +136 -0
  170. package/dist/src/test/mock-streams.js.map +1 -0
  171. package/dist/src/test/render-tui.d.ts +47 -0
  172. package/dist/src/test/render-tui.d.ts.map +1 -0
  173. package/dist/src/test/render-tui.js +76 -0
  174. package/dist/src/test/render-tui.js.map +1 -0
  175. package/dist/src/trace/SpanTree.d.ts +10 -0
  176. package/dist/src/trace/SpanTree.d.ts.map +1 -0
  177. package/dist/src/trace/SpanTree.js +104 -0
  178. package/dist/src/trace/SpanTree.js.map +1 -0
  179. package/dist/src/trace/index.d.ts +30 -0
  180. package/dist/src/trace/index.d.ts.map +1 -0
  181. package/dist/src/trace/index.js +142 -0
  182. package/dist/src/trace/index.js.map +1 -0
  183. package/dist/src/trace/location.d.ts +9 -0
  184. package/dist/src/trace/location.d.ts.map +1 -0
  185. package/dist/src/trace/location.js +88 -0
  186. package/dist/src/trace/location.js.map +1 -0
  187. package/dist/src/trace/span-processor.d.ts +16 -0
  188. package/dist/src/trace/span-processor.d.ts.map +1 -0
  189. package/dist/src/trace/span-processor.js +54 -0
  190. package/dist/src/trace/span-processor.js.map +1 -0
  191. package/dist/src/trace/span-state.d.ts +79 -0
  192. package/dist/src/trace/span-state.d.ts.map +1 -0
  193. package/dist/src/trace/span-state.js +229 -0
  194. package/dist/src/trace/span-state.js.map +1 -0
  195. package/dist/src/trace/tui-logger.d.ts +8 -0
  196. package/dist/src/trace/tui-logger.d.ts.map +1 -0
  197. package/dist/src/trace/tui-logger.js +70 -0
  198. package/dist/src/trace/tui-logger.js.map +1 -0
  199. package/dist/src/utils/border.d.ts +31 -0
  200. package/dist/src/utils/border.d.ts.map +1 -0
  201. package/dist/src/utils/border.js +81 -0
  202. package/dist/src/utils/border.js.map +1 -0
  203. package/dist/src/utils/flex-layout.d.ts +20 -0
  204. package/dist/src/utils/flex-layout.d.ts.map +1 -0
  205. package/dist/src/utils/flex-layout.js +85 -0
  206. package/dist/src/utils/flex-layout.js.map +1 -0
  207. package/dist/src/utils/index.d.ts +5 -0
  208. package/dist/src/utils/index.d.ts.map +1 -0
  209. package/dist/src/utils/index.js +5 -0
  210. package/dist/src/utils/index.js.map +1 -0
  211. package/dist/src/utils/padding.d.ts +26 -0
  212. package/dist/src/utils/padding.d.ts.map +1 -0
  213. package/dist/src/utils/padding.js +34 -0
  214. package/dist/src/utils/padding.js.map +1 -0
  215. package/dist/src/utils/styles.d.ts +13 -0
  216. package/dist/src/utils/styles.d.ts.map +1 -0
  217. package/dist/src/utils/styles.js +5 -0
  218. package/dist/src/utils/styles.js.map +1 -0
  219. package/dist/src/visualize/index.d.ts +50 -0
  220. package/dist/src/visualize/index.d.ts.map +1 -0
  221. package/dist/src/visualize/index.js +194 -0
  222. package/dist/src/visualize/index.js.map +1 -0
  223. package/dist/tsconfig.tsbuildinfo +1 -0
  224. package/package.json +94 -0
  225. package/src/codeblock.tsx +47 -0
  226. package/src/constants.ts +2 -0
  227. package/src/debug/DiagnosticsPanel.tsx +38 -0
  228. package/src/highlight.ts +76 -0
  229. package/src/hooks/index.ts +3 -0
  230. package/src/hooks/use-keyboard.ts +37 -0
  231. package/src/hooks/use-paste.ts +14 -0
  232. package/src/hooks/useFrameStats.ts +32 -0
  233. package/src/hosts/base.ts +65 -0
  234. package/src/hosts/box.ts +105 -0
  235. package/src/hosts/canvas.ts +155 -0
  236. package/src/hosts/codeblock.ts +145 -0
  237. package/src/hosts/hstack.ts +64 -0
  238. package/src/hosts/index.ts +45 -0
  239. package/src/hosts/spacer.ts +40 -0
  240. package/src/hosts/text.ts +175 -0
  241. package/src/hosts/vstack.ts +64 -0
  242. package/src/hosts/zstack.ts +77 -0
  243. package/src/index.ts +62 -0
  244. package/src/inline/index.tsx +181 -0
  245. package/src/jsx.ts +3 -0
  246. package/src/motion/color-motion-value.ts +90 -0
  247. package/src/motion/color.test.ts +115 -0
  248. package/src/motion/color.ts +191 -0
  249. package/src/motion/event-emitter.ts +35 -0
  250. package/src/motion/frame.ts +59 -0
  251. package/src/motion/hooks.ts +237 -0
  252. package/src/motion/index.ts +17 -0
  253. package/src/motion/motion-value.test.ts +222 -0
  254. package/src/motion/motion-value.ts +140 -0
  255. package/src/motion/spring-math.ts +114 -0
  256. package/src/motion/types.ts +34 -0
  257. package/src/output.ts +156 -0
  258. package/src/profiler.ts +88 -0
  259. package/src/reconciler/host-config.ts +277 -0
  260. package/src/reconciler/types.ts +66 -0
  261. package/src/renderer.ts +661 -0
  262. package/src/terminal.ts +67 -0
  263. package/src/test/index.ts +8 -0
  264. package/src/test/mock-streams.ts +149 -0
  265. package/src/test/render-tui.ts +118 -0
  266. package/src/trace/SpanTree.tsx +195 -0
  267. package/src/trace/index.tsx +205 -0
  268. package/src/trace/location.ts +90 -0
  269. package/src/trace/span-processor.ts +65 -0
  270. package/src/trace/span-state.ts +286 -0
  271. package/src/trace/tui-logger.ts +72 -0
  272. package/src/utils/border.ts +108 -0
  273. package/src/utils/flex-layout.ts +125 -0
  274. package/src/utils/index.ts +4 -0
  275. package/src/utils/padding.ts +45 -0
  276. package/src/utils/styles.ts +14 -0
  277. package/src/visualize/index.tsx +305 -0
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@effect-tui/react",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "React bindings for @effect-tui/core",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "src",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/src/index.js",
15
+ "types": "./dist/src/index.d.ts"
16
+ },
17
+ "./test": {
18
+ "import": "./dist/src/test/index.js",
19
+ "types": "./dist/src/test/index.d.ts"
20
+ },
21
+ "./visualize": {
22
+ "import": "./dist/src/visualize/index.js",
23
+ "types": "./dist/src/visualize/index.d.ts"
24
+ },
25
+ "./trace": {
26
+ "import": "./dist/src/trace/index.js",
27
+ "types": "./dist/src/trace/index.d.ts"
28
+ },
29
+ "./inline": {
30
+ "import": "./dist/src/inline/index.js",
31
+ "types": "./dist/src/inline/index.d.ts"
32
+ },
33
+ "./jsx-runtime": {
34
+ "import": "./dist/jsx-runtime.js",
35
+ "types": "./dist/jsx-runtime.d.ts"
36
+ },
37
+ "./jsx-dev-runtime": {
38
+ "import": "./dist/jsx-dev-runtime.js",
39
+ "types": "./dist/jsx-dev-runtime.d.ts"
40
+ }
41
+ },
42
+ "types": "./dist/src/index.d.ts",
43
+ "keywords": [
44
+ "terminal",
45
+ "tui",
46
+ "effect",
47
+ "react",
48
+ "cli"
49
+ ],
50
+ "author": "Kit Langton",
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/kitlangton/effect-tui.git",
55
+ "directory": "packages/effect-tui-react"
56
+ },
57
+ "homepage": "https://github.com/kitlangton/effect-tui",
58
+ "bugs": "https://github.com/kitlangton/effect-tui/issues",
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "tag": "alpha"
62
+ },
63
+ "scripts": {
64
+ "build": "tsc -p .",
65
+ "typecheck": "tsc -p . --noEmit",
66
+ "test": "vitest run",
67
+ "test:watch": "vitest",
68
+ "format": "biome format --write .",
69
+ "format:check": "biome format .",
70
+ "prepublishOnly": "bun run typecheck && bun run build"
71
+ },
72
+ "dependencies": {
73
+ "@effect/opentelemetry": "^0.59.1",
74
+ "@opentelemetry/api": "^1.9.0",
75
+ "@opentelemetry/sdk-trace-base": "^2.2.0",
76
+ "@effect-tui/core": "workspace:*",
77
+ "react-reconciler": "^0.33.0",
78
+ "shiki": "^3.17.0"
79
+ },
80
+ "peerDependencies": {
81
+ "effect": "^3.0.0",
82
+ "react": "^18.0.0 || ^19.0.0"
83
+ },
84
+ "devDependencies": {
85
+ "@effect/vitest": "^0.27.0",
86
+ "@types/node": "^24.10.1",
87
+ "@types/react": "^19.0.0",
88
+ "@types/react-reconciler": "^0.32.3",
89
+ "effect": "^3.19.8",
90
+ "react": "^19.0.0",
91
+ "typescript": "^5.9.3",
92
+ "vitest": "^4.0.14"
93
+ }
94
+ }
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from "react"
2
+ import type { BundledLanguage, BundledTheme } from "shiki"
3
+ import { highlightCode, toPlainLines, type HighlightLine } from "./highlight.js"
4
+ import type { CodeBlockProps as HostCodeBlockProps } from "./hosts/codeblock.js"
5
+
6
+ export interface CodeBlockProps extends Omit<HostCodeBlockProps, "lines"> {
7
+ code: string
8
+ language?: BundledLanguage
9
+ theme?: BundledTheme
10
+ }
11
+
12
+ export function CodeBlock({
13
+ code,
14
+ language = "ts",
15
+ theme = "nord",
16
+ lineNumbers = true,
17
+ padding = 1,
18
+ background,
19
+ ...rest
20
+ }: CodeBlockProps) {
21
+ const [lines, setLines] = useState<HighlightLine[]>(() => toPlainLines(code))
22
+
23
+ useEffect(() => {
24
+ let cancelled = false
25
+ highlightCode(code, { lang: language, theme })
26
+ .then((result) => {
27
+ if (!cancelled) setLines(result)
28
+ })
29
+ .catch((err) => {
30
+ console.warn("CodeBlock: highlighting failed, falling back to plain text", err)
31
+ if (!cancelled) setLines(toPlainLines(code))
32
+ })
33
+ return () => {
34
+ cancelled = true
35
+ }
36
+ }, [code, language, theme])
37
+
38
+ return (
39
+ <codeblock
40
+ {...(rest as Record<string, unknown>)}
41
+ lines={lines}
42
+ lineNumbers={lineNumbers as boolean}
43
+ padding={padding as HostCodeBlockProps["padding"]}
44
+ background={background as HostCodeBlockProps["background"]}
45
+ />
46
+ )
47
+ }
@@ -0,0 +1,2 @@
1
+ /** Default frames per second for renderer and animation loops */
2
+ export const DEFAULT_FPS = 60
@@ -0,0 +1,38 @@
1
+ import { Colors } from "@effect-tui/core"
2
+ import { useFrameStats } from "../hooks/useFrameStats.js"
3
+ import "../jsx.js"
4
+
5
+ export interface DiagnosticsPanelProps {
6
+ sampleMs?: number
7
+ title?: string
8
+ }
9
+
10
+ export function DiagnosticsPanel({ sampleMs = 200, title = "Diagnostics" }: DiagnosticsPanelProps) {
11
+ const stats = useFrameStats(sampleMs)
12
+
13
+ const fps = stats ? (stats.frameMs > 0 ? (1000 / stats.frameMs).toFixed(1) : "∞") : "—"
14
+ const bytes = stats ? stats.bytes : 0
15
+ const h = stats ? stats.contentHeight : 0
16
+ const phases = stats?.phases
17
+
18
+ return (
19
+ <vstack spacing={1}>
20
+ <text fg={Colors.cyan} bold>
21
+ {title} ({stats?.mode ?? "?"})
22
+ </text>
23
+ {stats ? (
24
+ <>
25
+ <text fg={Colors.gray(12)}>
26
+ size {stats.width}×{stats.height} content {h} rows — {bytes} bytes/frame — {fps} fps
27
+ </text>
28
+ <text fg={Colors.gray(12)}>
29
+ clear {phases?.clear.toFixed(2)}ms · layout {phases?.layout.toFixed(2)}ms · render{" "}
30
+ {phases?.render.toFixed(2)}ms · diff {phases?.diffAnsi.toFixed(2)}ms · write {phases?.write.toFixed(2)}ms
31
+ </text>
32
+ </>
33
+ ) : (
34
+ <text fg={Colors.gray(10)}>Waiting for first frame…</text>
35
+ )}
36
+ </vstack>
37
+ )
38
+ }
@@ -0,0 +1,76 @@
1
+ import { createHighlighter, type Highlighter, type BundledLanguage, type BundledTheme } from "shiki"
2
+ import type { ColorLike } from "@effect-tui/core"
3
+
4
+ export interface HighlightTokenStyle {
5
+ fg?: ColorLike
6
+ bg?: ColorLike
7
+ bold?: boolean
8
+ italic?: boolean
9
+ underline?: boolean
10
+ }
11
+
12
+ export interface HighlightToken {
13
+ text: string
14
+ style?: HighlightTokenStyle
15
+ }
16
+
17
+ export type HighlightLine = HighlightToken[]
18
+
19
+ const DEFAULT_LANGS: BundledLanguage[] = ["ts", "tsx", "js", "jsx", "json"]
20
+ const DEFAULT_THEME: BundledTheme = "nord"
21
+
22
+ const highlighterCache = new Map<string, Promise<Highlighter>>()
23
+
24
+ async function getCachedHighlighter(theme: BundledTheme): Promise<Highlighter> {
25
+ const key = String(theme)
26
+ let cached = highlighterCache.get(key)
27
+ if (!cached) {
28
+ cached = createHighlighter({
29
+ themes: [theme],
30
+ langs: DEFAULT_LANGS,
31
+ })
32
+ highlighterCache.set(key, cached)
33
+ }
34
+ return cached
35
+ }
36
+
37
+ export async function highlightCode(
38
+ code: string,
39
+ opts?: { lang?: BundledLanguage; theme?: BundledTheme },
40
+ ): Promise<HighlightLine[]> {
41
+ const lang = opts?.lang ?? ("ts" satisfies BundledLanguage)
42
+ const theme = opts?.theme ?? DEFAULT_THEME
43
+
44
+ const highlighter = await getCachedHighlighter(theme)
45
+
46
+ if (!highlighter.getLoadedLanguages().includes(lang)) {
47
+ await highlighter.loadLanguage(lang)
48
+ }
49
+
50
+ const tokensResult = await highlighter.codeToTokens(code, { lang, theme })
51
+ const tokenLines = Array.isArray(tokensResult)
52
+ ? tokensResult
53
+ : // Shiki v3 returns { tokens, theme }
54
+ (tokensResult as any).tokens
55
+
56
+ if (!Array.isArray(tokenLines)) return toPlainLines(code)
57
+
58
+ return tokenLines.map((line: any[]) =>
59
+ line.map((token) => {
60
+ const style: HighlightTokenStyle = {}
61
+ if (token.color) style.fg = token.color
62
+ const fs = token.fontStyle ?? 0
63
+ // fontStyle bitmask is: 1 = Italic, 2 = Bold, 4 = Underline
64
+ if (fs & 2) style.bold = true
65
+ if (fs & 1) style.italic = true
66
+ if (fs & 4) style.underline = true
67
+
68
+ return Object.keys(style).length > 0 ? { text: token.content, style } : { text: token.content }
69
+ }),
70
+ )
71
+ }
72
+
73
+ export function toPlainLines(code: string): HighlightLine[] {
74
+ if (code.length === 0) return [[]]
75
+ return code.split(/\r?\n/).map((line) => [{ text: line }])
76
+ }
@@ -0,0 +1,3 @@
1
+ export { useKeyboard } from "./use-keyboard.js"
2
+ export type { UseKeyboardOptions } from "./use-keyboard.js"
3
+ export { usePaste } from "./use-paste.js"
@@ -0,0 +1,37 @@
1
+ import { useEffect } from "react"
2
+ import type { KeyMsg } from "@effect-tui/core"
3
+ import { useRenderer } from "../renderer.js"
4
+
5
+ export type UseKeyboardOptions = {
6
+ /** Which phase to listen for; defaults to "press" and treats missing phase as "press". */
7
+ phase?: "press" | "repeat" | "release" | "any"
8
+ /** Optional predicate to drop keys before they reach the handler. */
9
+ filter?: (key: KeyMsg) => boolean
10
+ /**
11
+ * If true, call preventDefault when available before invoking handler.
12
+ * This stops further renderer-level handlers (not a terminal effect).
13
+ */
14
+ stopPropagation?: boolean
15
+ }
16
+
17
+ /**
18
+ * Subscribe to keyboard events.
19
+ * Handler is called for every key press while mounted.
20
+ */
21
+ export function useKeyboard(handler: (key: KeyMsg) => void, opts?: UseKeyboardOptions): void {
22
+ const renderer = useRenderer()
23
+ const phase = opts?.phase ?? "press"
24
+ const filter = opts?.filter
25
+ const stopPropagation = opts?.stopPropagation ?? false
26
+
27
+ useEffect(() => {
28
+ const wrapped = (key: KeyMsg) => {
29
+ const keyPhase = key.phase ?? "press"
30
+ if (phase !== "any" && phase !== keyPhase) return
31
+ if (filter && !filter(key)) return
32
+ if (stopPropagation && key.preventDefault) key.preventDefault()
33
+ handler(key)
34
+ }
35
+ return renderer.onKey(wrapped)
36
+ }, [renderer, handler, phase, filter, stopPropagation])
37
+ }
@@ -0,0 +1,14 @@
1
+ import { useEffect } from "react"
2
+ import { useRenderer } from "../renderer.js"
3
+
4
+ /**
5
+ * Subscribe to bracketed paste events (if supported by renderer/terminal).
6
+ */
7
+ export function usePaste(handler: (text: string) => void): void {
8
+ const renderer = useRenderer()
9
+
10
+ useEffect(() => {
11
+ if (!renderer.onPaste) return
12
+ return renderer.onPaste(handler)
13
+ }, [renderer, handler])
14
+ }
@@ -0,0 +1,32 @@
1
+ import { useEffect, useState } from "react"
2
+ import { useRenderer } from "../renderer.js"
3
+ import type { FrameStats } from "../renderer.js"
4
+
5
+ /**
6
+ * Subscribe to per-frame renderer stats (if enabled).
7
+ * Falls back to null when the renderer does not support onFrameStats.
8
+ */
9
+ export function useFrameStats(sampleMs = 200): FrameStats | null {
10
+ const renderer = useRenderer()
11
+ const [stats, setStats] = useState<FrameStats | null>(null)
12
+
13
+ useEffect(() => {
14
+ if (!renderer.onFrameStats) return
15
+
16
+ let last: FrameStats | null = null
17
+ const unsub = renderer.onFrameStats((s) => {
18
+ last = s
19
+ })
20
+
21
+ const id = setInterval(() => {
22
+ if (last) setStats(last)
23
+ }, sampleMs)
24
+
25
+ return () => {
26
+ clearInterval(id)
27
+ unsub?.()
28
+ }
29
+ }, [renderer, sampleMs])
30
+
31
+ return stats
32
+ }
@@ -0,0 +1,65 @@
1
+ import type { CellBuffer, Palette } from "@effect-tui/core"
2
+ import type { HostInstance, Rect, Size, HostContext, CommonProps } from "../reconciler/types.js"
3
+
4
+ let idCounter = 0
5
+
6
+ export abstract class BaseHost implements HostInstance {
7
+ id: string
8
+ type: string
9
+ parent: HostInstance | null = null
10
+ children: HostInstance[] = []
11
+ rect: Rect | null = null
12
+
13
+ // Common flex props
14
+ flexGrow = 0
15
+ flexShrink = 1
16
+
17
+ protected ctx: HostContext
18
+
19
+ constructor(type: string, props: CommonProps, ctx: HostContext) {
20
+ this.id = `${type}-${idCounter++}`
21
+ this.type = type
22
+ this.ctx = ctx
23
+ this.updateProps(props)
24
+ }
25
+
26
+ abstract measure(maxW: number, maxH: number): Size
27
+ abstract render(buffer: CellBuffer, palette: Palette): void
28
+
29
+ layout(rect: Rect): void {
30
+ this.rect = rect
31
+ }
32
+
33
+ updateProps(props: Record<string, unknown>): void {
34
+ if (props.flexGrow !== undefined) this.flexGrow = props.flexGrow as number
35
+ if (props.flexShrink !== undefined) this.flexShrink = props.flexShrink as number
36
+ }
37
+
38
+ destroy(): void {
39
+ // Override in subclasses if cleanup needed
40
+ }
41
+
42
+ // Child management helpers
43
+ appendChild(child: HostInstance): void {
44
+ this.children.push(child)
45
+ child.parent = this
46
+ }
47
+
48
+ removeChild(child: HostInstance): void {
49
+ const idx = this.children.indexOf(child)
50
+ if (idx >= 0) {
51
+ this.children.splice(idx, 1)
52
+ child.parent = null
53
+ }
54
+ }
55
+
56
+ insertBefore(child: HostInstance, before: HostInstance): void {
57
+ const idx = this.children.indexOf(before)
58
+ if (idx >= 0) {
59
+ this.children.splice(idx, 0, child)
60
+ } else {
61
+ this.children.push(child)
62
+ }
63
+ child.parent = this
64
+ }
65
+ }
@@ -0,0 +1,105 @@
1
+ import type { CellBuffer, Palette, ColorValue } from "@effect-tui/core"
2
+ import { Colors } from "@effect-tui/core"
3
+ import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
4
+ import { BaseHost } from "./base.js"
5
+ import { type BorderKind, borderChars, drawBorder, type Padding, type PaddingInput, resolvePadding } from "../utils/index.js"
6
+
7
+ export type { BorderKind }
8
+
9
+ export interface BoxProps extends CommonProps {
10
+ padding?: PaddingInput
11
+ border?: BorderKind
12
+ borderColor?: ColorValue
13
+ bg?: ColorValue
14
+ }
15
+
16
+ export class BoxHost extends BaseHost {
17
+ padding: Padding = { top: 0, right: 0, bottom: 0, left: 0 }
18
+ border: BorderKind = "none"
19
+ borderColor?: ColorValue
20
+ bg?: ColorValue
21
+
22
+ constructor(props: BoxProps, ctx: HostContext) {
23
+ super("box", props, ctx)
24
+ this.updateProps(props)
25
+ }
26
+
27
+ private get borderThickness(): number {
28
+ return this.border === "none" ? 0 : 1
29
+ }
30
+
31
+ private get insetX(): number {
32
+ return this.borderThickness + this.padding.left + this.padding.right + this.borderThickness
33
+ }
34
+
35
+ private get insetY(): number {
36
+ return this.borderThickness + this.padding.top + this.padding.bottom + this.borderThickness
37
+ }
38
+
39
+ measure(maxW: number, maxH: number): Size {
40
+ const innerMaxW = Math.max(0, maxW - this.insetX)
41
+ const innerMaxH = Math.max(0, maxH - this.insetY)
42
+
43
+ // Measure single child (box should have at most one child)
44
+ let childW = 0
45
+ let childH = 0
46
+ if (this.children.length > 0) {
47
+ const childSize = this.children[0].measure(innerMaxW, innerMaxH)
48
+ childW = childSize.w
49
+ childH = childSize.h
50
+ }
51
+
52
+ return {
53
+ w: childW + this.insetX,
54
+ h: childH + this.insetY,
55
+ }
56
+ }
57
+
58
+ override layout(rect: Rect): void {
59
+ super.layout(rect)
60
+
61
+ const t = this.borderThickness
62
+ const innerRect: Rect = {
63
+ x: rect.x + t + this.padding.left,
64
+ y: rect.y + t + this.padding.top,
65
+ w: Math.max(0, rect.w - this.insetX),
66
+ h: Math.max(0, rect.h - this.insetY),
67
+ }
68
+
69
+ // Layout single child
70
+ if (this.children.length > 0) {
71
+ this.children[0].layout(innerRect)
72
+ }
73
+ }
74
+
75
+ render(buffer: CellBuffer, palette: Palette): void {
76
+ if (!this.rect) return
77
+ const { x, y, w, h } = this.rect
78
+
79
+ // Draw background if set
80
+ if (this.bg !== undefined) {
81
+ const bgStyle = palette.id({ bg: this.bg })
82
+ buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyle)
83
+ }
84
+
85
+ // Draw border
86
+ if (this.border !== "none" && w >= 2 && h >= 2) {
87
+ const chars = borderChars(this.border)
88
+ const borderStyle = palette.id({ fg: this.borderColor ?? Colors.gray(8) })
89
+ drawBorder(buffer, x, y, w, h, chars, borderStyle)
90
+ }
91
+
92
+ // Render children
93
+ for (const child of this.children) {
94
+ child.render(buffer, palette)
95
+ }
96
+ }
97
+
98
+ override updateProps(props: Record<string, unknown>): void {
99
+ super.updateProps(props)
100
+ if (props.padding !== undefined) this.padding = resolvePadding(props.padding as BoxProps["padding"])
101
+ if (props.border !== undefined) this.border = props.border as BorderKind
102
+ if (props.borderColor !== undefined) this.borderColor = props.borderColor as ColorValue
103
+ if (props.bg !== undefined) this.bg = props.bg as ColorValue
104
+ }
105
+ }
@@ -0,0 +1,155 @@
1
+ import type { CellBuffer, Palette, ColorValue } from "@effect-tui/core"
2
+ import { Colors } from "@effect-tui/core"
3
+ import type { HostContext, Size, CommonProps } from "../reconciler/types.js"
4
+ import { BaseHost } from "./base.js"
5
+ import { type BorderKind, borderChars, drawBorder } from "../utils/index.js"
6
+
7
+ export type { BorderKind }
8
+
9
+ export interface DrawContext {
10
+ /** Canvas width in cells */
11
+ width: number
12
+ /** Canvas height in cells */
13
+ height: number
14
+
15
+ /** Draw text at position */
16
+ text(x: number, y: number, str: string, opts?: { fg?: ColorValue; bg?: ColorValue }): void
17
+
18
+ /** Fill rectangle with character */
19
+ fill(x: number, y: number, w: number, h: number, char?: string, opts?: { fg?: ColorValue; bg?: ColorValue }): void
20
+
21
+ /** Draw box with optional border */
22
+ box(
23
+ x: number,
24
+ y: number,
25
+ w: number,
26
+ h: number,
27
+ opts?: {
28
+ border?: BorderKind
29
+ borderColor?: ColorValue
30
+ bg?: ColorValue
31
+ fg?: ColorValue
32
+ },
33
+ ): void
34
+
35
+ /** Clear entire canvas */
36
+ clear(): void
37
+ }
38
+
39
+ export interface CanvasProps extends CommonProps {
40
+ /** Draw function called each render */
41
+ draw: (ctx: DrawContext) => void
42
+ /** Fixed width (default: fill available) */
43
+ width?: number
44
+ /** Fixed height (default: fill available) */
45
+ height?: number
46
+ }
47
+
48
+ export class CanvasHost extends BaseHost {
49
+ draw: CanvasProps["draw"] = () => {}
50
+ fixedWidth?: number
51
+ fixedHeight?: number
52
+
53
+ constructor(props: CanvasProps, ctx: HostContext) {
54
+ super("canvas", props, ctx)
55
+ this.updateProps(props)
56
+ }
57
+
58
+ measure(maxW: number, maxH: number): Size {
59
+ return {
60
+ w: this.fixedWidth ?? maxW,
61
+ h: this.fixedHeight ?? maxH,
62
+ }
63
+ }
64
+
65
+ render(buffer: CellBuffer, palette: Palette): void {
66
+ if (!this.rect) return
67
+ const { x: ox, y: oy, w, h } = this.rect
68
+
69
+ // Create draw context
70
+ const ctx: DrawContext = {
71
+ width: w,
72
+ height: h,
73
+
74
+ text: (x, y, str, opts) => {
75
+ const px = Math.round(ox + x)
76
+ const py = Math.round(oy + y)
77
+ if (py < oy || py >= oy + h) return
78
+ const style = palette.id({ fg: opts?.fg, bg: opts?.bg })
79
+ let col = px
80
+ for (const char of str) {
81
+ if (col >= ox + w) break
82
+ if (col >= ox) {
83
+ buffer.drawCP(col, py, char.codePointAt(0)!, style)
84
+ }
85
+ col++
86
+ }
87
+ },
88
+
89
+ fill: (x, y, fw, fh, char = " ", opts) => {
90
+ const px = Math.round(ox + x)
91
+ const py = Math.round(oy + y)
92
+ const cp = char.codePointAt(0)!
93
+ const style = palette.id({ fg: opts?.fg, bg: opts?.bg })
94
+ for (let row = 0; row < fh; row++) {
95
+ const yy = py + row
96
+ if (yy < oy || yy >= oy + h) continue
97
+ for (let col = 0; col < fw; col++) {
98
+ const xx = px + col
99
+ if (xx < ox || xx >= ox + w) continue
100
+ buffer.drawCP(xx, yy, cp, style)
101
+ }
102
+ }
103
+ },
104
+
105
+ box: (x, y, bw, bh, opts) => {
106
+ const px = Math.round(ox + x)
107
+ const py = Math.round(oy + y)
108
+ const border = opts?.border ?? "none"
109
+ const bgStyle = opts?.bg !== undefined ? palette.id({ bg: opts.bg }) : undefined
110
+
111
+ // Fill background (with clipping)
112
+ if (bgStyle !== undefined) {
113
+ for (let row = 0; row < bh; row++) {
114
+ const yy = py + row
115
+ if (yy < oy || yy >= oy + h) continue
116
+ for (let col = 0; col < bw; col++) {
117
+ const xx = px + col
118
+ if (xx < ox || xx >= ox + w) continue
119
+ buffer.drawCP(xx, yy, " ".codePointAt(0)!, bgStyle)
120
+ }
121
+ }
122
+ }
123
+
124
+ // Draw border (with clipping)
125
+ if (border !== "none" && bw >= 2 && bh >= 2) {
126
+ const chars = borderChars(border)
127
+ const borderStyle = palette.id({ fg: opts?.borderColor ?? opts?.fg ?? Colors.gray(8) })
128
+ drawBorder(buffer, px, py, bw, bh, chars, borderStyle, { ox, oy, w, h })
129
+ }
130
+ },
131
+
132
+ clear: () => {
133
+ const style = palette.id({})
134
+ for (let row = 0; row < h; row++) {
135
+ for (let col = 0; col < w; col++) {
136
+ buffer.drawCP(ox + col, oy + row, " ".codePointAt(0)!, style)
137
+ }
138
+ }
139
+ },
140
+ }
141
+
142
+ // Call user's draw function
143
+ this.draw(ctx)
144
+ }
145
+
146
+ override updateProps(props: Record<string, unknown>): void {
147
+ super.updateProps(props)
148
+ if (props.draw !== undefined) {
149
+ this.draw = props.draw as CanvasProps["draw"]
150
+ this.ctx.requestRender() // trigger repaint when draw function changes
151
+ }
152
+ if (props.width !== undefined) this.fixedWidth = props.width as number
153
+ if (props.height !== undefined) this.fixedHeight = props.height as number
154
+ }
155
+ }