@effect-tui/react 0.15.2 → 0.16.0

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 (249) hide show
  1. package/README.md +2 -2
  2. package/dist/src/components/ListView.d.ts +4 -4
  3. package/dist/src/components/ListView.d.ts.map +1 -1
  4. package/dist/src/components/ListView.js +16 -17
  5. package/dist/src/components/ListView.js.map +1 -1
  6. package/dist/src/console/ConsolePopover.d.ts +7 -1
  7. package/dist/src/console/ConsolePopover.d.ts.map +1 -1
  8. package/dist/src/console/ConsolePopover.js +55 -74
  9. package/dist/src/console/ConsolePopover.js.map +1 -1
  10. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  11. package/dist/src/debug/DebugOverlay.js +3 -57
  12. package/dist/src/debug/DebugOverlay.js.map +1 -1
  13. package/dist/src/debug/DiagnosticsPanel.js +1 -1
  14. package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
  15. package/dist/src/dev.d.ts +5 -117
  16. package/dist/src/dev.d.ts.map +1 -1
  17. package/dist/src/dev.js +3 -333
  18. package/dist/src/dev.js.map +1 -1
  19. package/dist/src/hooks/use-scroll.d.ts +31 -35
  20. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  21. package/dist/src/hooks/use-scroll.js +51 -90
  22. package/dist/src/hooks/use-scroll.js.map +1 -1
  23. package/dist/src/hosts/canvas.d.ts +2 -2
  24. package/dist/src/hosts/canvas.d.ts.map +1 -1
  25. package/dist/src/hosts/canvas.js +8 -10
  26. package/dist/src/hosts/canvas.js.map +1 -1
  27. package/dist/src/hosts/codeblock.d.ts +2 -2
  28. package/dist/src/hosts/codeblock.js +2 -2
  29. package/dist/src/hosts/flex-container.d.ts +1 -1
  30. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  31. package/dist/src/hosts/flex-container.js +3 -3
  32. package/dist/src/hosts/flex-container.js.map +1 -1
  33. package/dist/src/hosts/index.d.ts +2 -1
  34. package/dist/src/hosts/index.d.ts.map +1 -1
  35. package/dist/src/hosts/index.js +2 -1
  36. package/dist/src/hosts/index.js.map +1 -1
  37. package/dist/src/hosts/layout-helpers.d.ts +10 -0
  38. package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
  39. package/dist/src/hosts/layout-helpers.js +10 -0
  40. package/dist/src/hosts/layout-helpers.js.map +1 -0
  41. package/dist/src/hosts/leaf.d.ts +14 -0
  42. package/dist/src/hosts/leaf.d.ts.map +1 -0
  43. package/dist/src/hosts/leaf.js +31 -0
  44. package/dist/src/hosts/leaf.js.map +1 -0
  45. package/dist/src/hosts/overlay.d.ts.map +1 -1
  46. package/dist/src/hosts/overlay.js +4 -7
  47. package/dist/src/hosts/overlay.js.map +1 -1
  48. package/dist/src/hosts/scroll.d.ts +47 -24
  49. package/dist/src/hosts/scroll.d.ts.map +1 -1
  50. package/dist/src/hosts/scroll.js +68 -51
  51. package/dist/src/hosts/scroll.js.map +1 -1
  52. package/dist/src/hosts/spacer.d.ts +2 -2
  53. package/dist/src/hosts/spacer.js +2 -2
  54. package/dist/src/hosts/text.d.ts +2 -3
  55. package/dist/src/hosts/text.d.ts.map +1 -1
  56. package/dist/src/hosts/text.js +5 -61
  57. package/dist/src/hosts/text.js.map +1 -1
  58. package/dist/src/hosts/vstack.js +1 -1
  59. package/dist/src/hosts/vstack.js.map +1 -1
  60. package/dist/src/hosts/zstack.d.ts +1 -1
  61. package/dist/src/hosts/zstack.d.ts.map +1 -1
  62. package/dist/src/hosts/zstack.js +6 -6
  63. package/dist/src/hosts/zstack.js.map +1 -1
  64. package/dist/src/index.d.ts +1 -1
  65. package/dist/src/index.d.ts.map +1 -1
  66. package/dist/src/internal/dev/hmr.d.ts +20 -0
  67. package/dist/src/internal/dev/hmr.d.ts.map +1 -0
  68. package/dist/src/internal/dev/hmr.js +93 -0
  69. package/dist/src/internal/dev/hmr.js.map +1 -0
  70. package/dist/src/internal/dev/runtime.d.ts +24 -0
  71. package/dist/src/internal/dev/runtime.d.ts.map +1 -0
  72. package/dist/src/internal/dev/runtime.js +135 -0
  73. package/dist/src/internal/dev/runtime.js.map +1 -0
  74. package/dist/src/internal/dev/ui.d.ts +13 -0
  75. package/dist/src/internal/dev/ui.d.ts.map +1 -0
  76. package/dist/src/internal/dev/ui.js +51 -0
  77. package/dist/src/internal/dev/ui.js.map +1 -0
  78. package/dist/src/internal/renderer/context.d.ts +9 -0
  79. package/dist/src/internal/renderer/context.d.ts.map +1 -0
  80. package/dist/src/internal/renderer/context.js +22 -0
  81. package/dist/src/internal/renderer/context.js.map +1 -0
  82. package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
  83. package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
  84. package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
  85. package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
  86. package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
  87. package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
  88. package/dist/src/internal/renderer/core/RendererState.js +70 -0
  89. package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
  90. package/dist/src/internal/renderer/core/index.d.ts +3 -0
  91. package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
  92. package/dist/src/internal/renderer/core/index.js +3 -0
  93. package/dist/src/internal/renderer/core/index.js.map +1 -0
  94. package/dist/src/internal/renderer/index.d.ts +40 -0
  95. package/dist/src/internal/renderer/index.d.ts.map +1 -0
  96. package/dist/src/internal/renderer/index.js +518 -0
  97. package/dist/src/internal/renderer/index.js.map +1 -0
  98. package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
  99. package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
  100. package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
  101. package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
  102. package/dist/src/internal/renderer/input/index.d.ts +2 -0
  103. package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
  104. package/dist/src/internal/renderer/input/index.js +2 -0
  105. package/dist/src/internal/renderer/input/index.js.map +1 -0
  106. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
  107. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
  108. package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
  109. package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
  110. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
  111. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
  112. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
  113. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
  114. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
  115. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
  116. package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
  117. package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
  118. package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
  119. package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
  120. package/dist/src/internal/renderer/lifecycle/index.js +4 -0
  121. package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
  122. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
  123. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
  124. package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
  125. package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
  126. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
  127. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
  128. package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
  129. package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
  130. package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
  131. package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
  132. package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
  133. package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
  134. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
  135. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
  136. package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
  137. package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
  138. package/dist/src/internal/renderer/modes/index.d.ts +5 -0
  139. package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
  140. package/dist/src/internal/renderer/modes/index.js +4 -0
  141. package/dist/src/internal/renderer/modes/index.js.map +1 -0
  142. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
  143. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
  144. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
  145. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
  146. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
  147. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
  148. package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
  149. package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
  150. package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
  151. package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
  152. package/dist/src/internal/renderer/terminal/index.js +3 -0
  153. package/dist/src/internal/renderer/terminal/index.js.map +1 -0
  154. package/dist/src/internal/renderer/types.d.ts +118 -0
  155. package/dist/src/internal/renderer/types.d.ts.map +1 -0
  156. package/dist/src/internal/renderer/types.js +2 -0
  157. package/dist/src/internal/renderer/types.js.map +1 -0
  158. package/dist/src/renderer-context.d.ts +1 -8
  159. package/dist/src/renderer-context.d.ts.map +1 -1
  160. package/dist/src/renderer-context.js +1 -21
  161. package/dist/src/renderer-context.js.map +1 -1
  162. package/dist/src/renderer-types.d.ts +1 -115
  163. package/dist/src/renderer-types.d.ts.map +1 -1
  164. package/dist/src/renderer.d.ts +1 -31
  165. package/dist/src/renderer.d.ts.map +1 -1
  166. package/dist/src/renderer.js +1 -495
  167. package/dist/src/renderer.js.map +1 -1
  168. package/dist/src/test/render-tui.d.ts +3 -3
  169. package/dist/src/test/render-tui.d.ts.map +1 -1
  170. package/dist/src/test/render-tui.js +16 -9
  171. package/dist/src/test/render-tui.js.map +1 -1
  172. package/dist/src/utils/alignment.d.ts +1 -1
  173. package/dist/src/utils/alignment.d.ts.map +1 -1
  174. package/dist/src/utils/alignment.js +0 -2
  175. package/dist/src/utils/alignment.js.map +1 -1
  176. package/dist/src/utils/console-helpers.d.ts +19 -0
  177. package/dist/src/utils/console-helpers.d.ts.map +1 -0
  178. package/dist/src/utils/console-helpers.js +61 -0
  179. package/dist/src/utils/console-helpers.js.map +1 -0
  180. package/dist/src/utils/index.d.ts +1 -1
  181. package/dist/src/utils/index.d.ts.map +1 -1
  182. package/dist/src/utils/index.js +1 -1
  183. package/dist/src/utils/index.js.map +1 -1
  184. package/dist/src/utils/styles.d.ts +8 -1
  185. package/dist/src/utils/styles.d.ts.map +1 -1
  186. package/dist/src/utils/styles.js +10 -8
  187. package/dist/src/utils/styles.js.map +1 -1
  188. package/dist/src/utils/text-wrap.d.ts +5 -0
  189. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  190. package/dist/src/utils/text-wrap.js +110 -48
  191. package/dist/src/utils/text-wrap.js.map +1 -1
  192. package/dist/src/visualize/index.js +1 -1
  193. package/dist/src/visualize/index.js.map +1 -1
  194. package/dist/tsconfig.tsbuildinfo +1 -1
  195. package/package.json +2 -2
  196. package/src/components/ListView.tsx +21 -23
  197. package/src/console/ConsolePopover.tsx +124 -107
  198. package/src/debug/DebugOverlay.ts +15 -74
  199. package/src/debug/DiagnosticsPanel.tsx +1 -1
  200. package/src/dev.tsx +5 -458
  201. package/src/hooks/use-scroll.ts +85 -145
  202. package/src/hosts/canvas.ts +8 -11
  203. package/src/hosts/codeblock.ts +2 -2
  204. package/src/hosts/flex-container.ts +4 -4
  205. package/src/hosts/index.ts +10 -1
  206. package/src/hosts/layout-helpers.ts +20 -0
  207. package/src/hosts/leaf.ts +36 -0
  208. package/src/hosts/overlay.ts +11 -9
  209. package/src/hosts/scroll.ts +94 -69
  210. package/src/hosts/spacer.ts +2 -2
  211. package/src/hosts/text.ts +5 -58
  212. package/src/hosts/vstack.ts +1 -1
  213. package/src/hosts/zstack.ts +7 -7
  214. package/src/index.ts +1 -1
  215. package/src/internal/dev/hmr.ts +101 -0
  216. package/src/internal/dev/runtime.ts +170 -0
  217. package/src/internal/dev/ui.tsx +87 -0
  218. package/src/internal/renderer/context.ts +27 -0
  219. package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
  220. package/src/internal/renderer/index.ts +656 -0
  221. package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
  222. package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
  223. package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
  224. package/src/internal/renderer/lifecycle/index.ts +3 -0
  225. package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
  226. package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
  227. package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
  228. package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
  229. package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
  230. package/src/internal/renderer/terminal/index.ts +2 -0
  231. package/src/internal/renderer/types.ts +125 -0
  232. package/src/renderer-context.ts +1 -27
  233. package/src/renderer-types.ts +10 -123
  234. package/src/renderer.ts +1 -619
  235. package/src/test/render-tui.ts +16 -10
  236. package/src/utils/alignment.ts +1 -3
  237. package/src/utils/console-helpers.ts +86 -0
  238. package/src/utils/index.ts +1 -1
  239. package/src/utils/styles.ts +16 -4
  240. package/src/utils/text-wrap.ts +139 -48
  241. package/src/visualize/index.tsx +1 -1
  242. package/src/renderer/lifecycle/ResizeManager.ts +0 -65
  243. package/src/renderer/lifecycle/index.ts +0 -4
  244. /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
  245. /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
  246. /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
  247. /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
  248. /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
  249. /package/src/{renderer → internal/renderer}/modes/index.ts +0 -0
@@ -0,0 +1,125 @@
1
+ type ExitHandler = () => void
2
+ type SignalHandler = (signal: NodeJS.Signals) => void
3
+ type UncaughtHandler = (err: Error) => void
4
+ type RejectionHandler = (reason: unknown) => void
5
+
6
+ export interface ProcessHandlers {
7
+ onExit?: ExitHandler
8
+ onSignal?: SignalHandler
9
+ onUncaughtException?: UncaughtHandler
10
+ onUnhandledRejection?: RejectionHandler
11
+ }
12
+
13
+ const exitHandlers = new Set<ExitHandler>()
14
+ const signalHandlers = new Set<SignalHandler>()
15
+ const uncaughtHandlers = new Set<UncaughtHandler>()
16
+ const rejectionHandlers = new Set<RejectionHandler>()
17
+
18
+ let exitListenerAttached = false
19
+ let signalListenersAttached = false
20
+ let uncaughtListenerAttached = false
21
+ let rejectionListenerAttached = false
22
+
23
+ const dispatchExit = () => {
24
+ for (const handler of exitHandlers) handler()
25
+ }
26
+
27
+ const dispatchSignal = (signal: NodeJS.Signals) => {
28
+ for (const handler of signalHandlers) handler(signal)
29
+ }
30
+
31
+ const dispatchUncaught = (err: Error) => {
32
+ for (const handler of uncaughtHandlers) handler(err)
33
+ }
34
+
35
+ const dispatchRejection = (reason: unknown) => {
36
+ for (const handler of rejectionHandlers) handler(reason)
37
+ }
38
+
39
+ const ensureExitListener = () => {
40
+ if (exitListenerAttached) return
41
+ process.on("exit", dispatchExit)
42
+ exitListenerAttached = true
43
+ }
44
+
45
+ const ensureSignalListeners = () => {
46
+ if (signalListenersAttached) return
47
+ process.on("SIGINT", dispatchSignal)
48
+ process.on("SIGTERM", dispatchSignal)
49
+ signalListenersAttached = true
50
+ }
51
+
52
+ const ensureUncaughtListener = () => {
53
+ if (uncaughtListenerAttached) return
54
+ process.on("uncaughtException", dispatchUncaught)
55
+ uncaughtListenerAttached = true
56
+ }
57
+
58
+ const ensureRejectionListener = () => {
59
+ if (rejectionListenerAttached) return
60
+ process.on("unhandledRejection", dispatchRejection)
61
+ rejectionListenerAttached = true
62
+ }
63
+
64
+ const maybeDetachExitListener = () => {
65
+ if (!exitListenerAttached || exitHandlers.size > 0) return
66
+ process.off("exit", dispatchExit)
67
+ exitListenerAttached = false
68
+ }
69
+
70
+ const maybeDetachSignalListeners = () => {
71
+ if (!signalListenersAttached || signalHandlers.size > 0) return
72
+ process.off("SIGINT", dispatchSignal)
73
+ process.off("SIGTERM", dispatchSignal)
74
+ signalListenersAttached = false
75
+ }
76
+
77
+ const maybeDetachUncaughtListener = () => {
78
+ if (!uncaughtListenerAttached || uncaughtHandlers.size > 0) return
79
+ process.off("uncaughtException", dispatchUncaught)
80
+ uncaughtListenerAttached = false
81
+ }
82
+
83
+ const maybeDetachRejectionListener = () => {
84
+ if (!rejectionListenerAttached || rejectionHandlers.size > 0) return
85
+ process.off("unhandledRejection", dispatchRejection)
86
+ rejectionListenerAttached = false
87
+ }
88
+
89
+ export const registerProcessHandlers = (handlers: ProcessHandlers): (() => void) => {
90
+ if (handlers.onExit) {
91
+ exitHandlers.add(handlers.onExit)
92
+ ensureExitListener()
93
+ }
94
+ if (handlers.onSignal) {
95
+ signalHandlers.add(handlers.onSignal)
96
+ ensureSignalListeners()
97
+ }
98
+ if (handlers.onUncaughtException) {
99
+ uncaughtHandlers.add(handlers.onUncaughtException)
100
+ ensureUncaughtListener()
101
+ }
102
+ if (handlers.onUnhandledRejection) {
103
+ rejectionHandlers.add(handlers.onUnhandledRejection)
104
+ ensureRejectionListener()
105
+ }
106
+
107
+ return () => {
108
+ if (handlers.onExit) {
109
+ exitHandlers.delete(handlers.onExit)
110
+ maybeDetachExitListener()
111
+ }
112
+ if (handlers.onSignal) {
113
+ signalHandlers.delete(handlers.onSignal)
114
+ maybeDetachSignalListeners()
115
+ }
116
+ if (handlers.onUncaughtException) {
117
+ uncaughtHandlers.delete(handlers.onUncaughtException)
118
+ maybeDetachUncaughtListener()
119
+ }
120
+ if (handlers.onUnhandledRejection) {
121
+ rejectionHandlers.delete(handlers.onUnhandledRejection)
122
+ maybeDetachRejectionListener()
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,3 @@
1
+ export { EventBus } from "./EventBus.js"
2
+ export { registerProcessHandlers, type ProcessHandlers } from "./ProcessLifecycle.js"
3
+ export { getRenderCache, type RenderCache } from "./RenderCache.js"
@@ -163,12 +163,15 @@ export class InlineRenderer implements RendererMode {
163
163
  return this._needsFullRerender
164
164
  }
165
165
 
166
- /** Clear the full rerender flag after handling */
166
+ /** Clear the full rerender flag after handling (keeps previousHeight for cursor tracking) */
167
167
  clearFullRerenderFlag(): void {
168
168
  this._needsFullRerender = false
169
- this.previousHeight = 0
169
+ // DON'T reset previousHeight - we need it for cursor positioning in generateOutput.
170
+ // After clear+replay static, the cursor is at end of static content.
171
+ // generateOutput will use previousHeight to know where dynamic content should go.
170
172
  this.previousStartRow = 0
171
173
  this.printedWidths = []
174
+ this._forceFullOutput = true // Force full output to resync rendering
172
175
  }
173
176
 
174
177
  reset(): void {
@@ -1,5 +1,5 @@
1
1
  import type { CellBuffer, Palette } from "@effect-tui/core"
2
- import type { TuiWriteStream } from "../../renderer-types.js"
2
+ import type { TuiWriteStream } from "../types.js"
3
3
 
4
4
  /**
5
5
  * Context passed to render mode output generation.
@@ -1,6 +1,6 @@
1
1
  import { CellBuffer, emitRowWithReset, type Palette, rowContentWidth } from "@effect-tui/core"
2
- import type { HostInstance } from "../../reconciler/types.js"
3
- import type { TuiWriteStream } from "../../renderer-types.js"
2
+ import type { HostInstance } from "../../../reconciler/types.js"
3
+ import type { TuiWriteStream } from "../types.js"
4
4
 
5
5
  /**
6
6
  * Renders static content to scrollback (inline mode only).
@@ -31,6 +31,9 @@ export class StaticContentRenderer {
31
31
  * Caller clears dynamic area first, then appends this output.
32
32
  */
33
33
  render(staticNode: HostInstance, frameWidth: number): string {
34
+ // Pre-frame cache prep (optional)
35
+ staticNode.prepareFrame?.()
36
+
34
37
  // Measure and layout static content
35
38
  const staticSize = staticNode.measure(frameWidth, Number.MAX_SAFE_INTEGER)
36
39
  staticNode.layout({ x: 0, y: 0, w: frameWidth, h: staticSize.h })
@@ -0,0 +1,91 @@
1
+ import { ANSI } from "@effect-tui/core"
2
+ import type { TuiWriteStream } from "../types.js"
3
+
4
+ type ProbeOptions = {
5
+ stdout: TuiWriteStream
6
+ timeoutMs?: number
7
+ onResolved: (supportsKitty: boolean) => void
8
+ }
9
+
10
+ export type KeyboardCapabilityProbe = {
11
+ start(): void
12
+ handleInput(input: string): string
13
+ }
14
+
15
+ const kittyResponseRe = /^\x1b\[\?\d+(?:;\d+)?u$/
16
+
17
+ export function createKeyboardCapabilityProbe(options: ProbeOptions): KeyboardCapabilityProbe {
18
+ const timeoutMs = options.timeoutMs ?? 150
19
+ let active = false
20
+ let resolved = false
21
+ let buffer = ""
22
+ let timer: ReturnType<typeof setTimeout> | null = null
23
+
24
+ const resolve = (supportsKitty: boolean) => {
25
+ if (resolved) return
26
+ resolved = true
27
+ active = false
28
+ buffer = ""
29
+ if (timer) {
30
+ clearTimeout(timer)
31
+ timer = null
32
+ }
33
+ options.onResolved(supportsKitty)
34
+ }
35
+
36
+ const handleCapabilitySequence = (sequence: string) => {
37
+ if (kittyResponseRe.test(sequence)) {
38
+ resolve(true)
39
+ } else if (sequence.includes("kitty")) {
40
+ resolve(true)
41
+ }
42
+ }
43
+
44
+ const handleInput = (input: string): string => {
45
+ if (!active) return input
46
+ let data = buffer + input
47
+ buffer = ""
48
+ let output = ""
49
+ let i = 0
50
+
51
+ while (i < data.length) {
52
+ if (data.startsWith("\x1b[?", i)) {
53
+ const end = data.indexOf("u", i + 3)
54
+ if (end === -1) {
55
+ buffer = data.slice(i)
56
+ break
57
+ }
58
+ const seq = data.slice(i, end + 1)
59
+ handleCapabilitySequence(seq)
60
+ i = end + 1
61
+ continue
62
+ }
63
+ if (data.startsWith("\x1bP>|", i)) {
64
+ const end = data.indexOf("\x1b\\", i + 4)
65
+ if (end === -1) {
66
+ buffer = data.slice(i)
67
+ break
68
+ }
69
+ const seq = data.slice(i, end + 2)
70
+ handleCapabilitySequence(seq)
71
+ i = end + 2
72
+ continue
73
+ }
74
+
75
+ output += data[i] as string
76
+ i += 1
77
+ }
78
+
79
+ return output
80
+ }
81
+
82
+ return {
83
+ start() {
84
+ if (resolved || active) return
85
+ active = true
86
+ options.stdout.write(ANSI.keyboard.query)
87
+ timer = setTimeout(() => resolve(false), timeoutMs)
88
+ },
89
+ handleInput,
90
+ }
91
+ }
@@ -1,19 +1,11 @@
1
1
  import { ANSI, Terminal } from "@effect-tui/core"
2
- import type { TuiReadStream, TuiWriteStream } from "../../renderer-types.js"
2
+ import type { TuiReadStream, TuiWriteStream } from "../types.js"
3
3
 
4
4
  export interface TerminalSetupConfig {
5
5
  mode: "fullscreen" | "inline"
6
6
  enablePaste: boolean
7
7
  enableMouse: boolean
8
8
  skipTerminalSetup: boolean
9
- /**
10
- * Enable Kitty keyboard protocol for enhanced modifier detection.
11
- * When enabled, Ctrl+Shift+C can be distinguished from Ctrl+C.
12
- * Supported by: Kitty, iTerm2 3.5+, WezTerm, Ghostty, foot, Contour, rio.
13
- * Gracefully ignored by terminals that don't support it.
14
- * @default true
15
- */
16
- enableKittyKeyboard?: boolean
17
9
  }
18
10
 
19
11
  /**
@@ -53,14 +45,6 @@ export class TerminalSetup {
53
45
  this.stdout.write(ANSI.mouse.enable)
54
46
  }
55
47
 
56
- // Enable enhanced keyboard protocols for modifier detection
57
- // This allows distinguishing Ctrl+Shift+C from Ctrl+C on supported terminals
58
- // NOTE: Many terminals intercept Ctrl+Shift+C for their own copy function!
59
- if (this.config.enableKittyKeyboard !== false) {
60
- this.stdout.write(ANSI.keyboard.enable(1)) // Kitty protocol (flag 1 = disambiguate)
61
- this.stdout.write(ANSI.modifyOtherKeys.enable) // xterm protocol (mode 2)
62
- }
63
-
64
48
  if (this.stdin.isTTY && this.stdin.setRawMode) {
65
49
  this.stdin.setRawMode(true)
66
50
  this.stdin.resume?.()
@@ -85,11 +69,9 @@ export class TerminalSetup {
85
69
  // This ensures all sequences are flushed together before process exit
86
70
  let output = ""
87
71
 
88
- // Disable enhanced keyboard protocols first
89
- if (this.config.enableKittyKeyboard !== false) {
90
- output += ANSI.keyboard.disable
91
- output += ANSI.modifyOtherKeys.disable
92
- }
72
+ // Disable enhanced keyboard protocols first (safe to send regardless of support)
73
+ output += ANSI.keyboard.disable
74
+ output += ANSI.modifyOtherKeys.disable
93
75
 
94
76
  if (this.config.enableMouse) {
95
77
  output += ANSI.mouse.disable
@@ -0,0 +1,2 @@
1
+ export { createKeyboardCapabilityProbe, type KeyboardCapabilityProbe } from "./KeyboardCapabilityProbe.js"
2
+ export { TerminalSetup, type TerminalSetupConfig } from "./TerminalSetup.js"
@@ -0,0 +1,125 @@
1
+ import type { KeyMsg, MouseMsg } from "@effect-tui/core"
2
+ import type { HostContext, HostInstance } from "../../reconciler/types.js"
3
+
4
+ export type PasteMsg = {
5
+ type: "paste"
6
+ text: string
7
+ /** Set by renderer to allow handlers to stop further propagation. */
8
+ defaultPrevented?: boolean
9
+ preventDefault?: () => void
10
+ }
11
+
12
+ /** Minimal write stream interface for renderer output */
13
+ export interface TuiWriteStream {
14
+ write(s: string): void
15
+ columns: number
16
+ rows: number
17
+ on(event: string, cb: () => void): void
18
+ removeListener(event: string, cb: () => void): void
19
+ }
20
+
21
+ /** Minimal read stream interface for renderer input */
22
+ export interface TuiReadStream {
23
+ isTTY?: boolean
24
+ setRawMode?(mode: boolean): void
25
+ resume?(): void
26
+ on(event: string, cb: (data: Buffer) => void): void
27
+ removeListener(event: string, cb: (data: Buffer) => void): void
28
+ }
29
+
30
+ export interface TuiRenderer {
31
+ /** Terminal width */
32
+ width: number
33
+ /** Terminal height */
34
+ height: number
35
+ /** Request a re-render */
36
+ requestRender(): void
37
+ /** Subscribe to per-frame stats (if enabled). */
38
+ onFrameStats?(handler: (stats: FrameStats) => void): () => void
39
+ /** Subscribe to keyboard events */
40
+ onKey(handler: (key: KeyMsg) => void): () => void
41
+ /** Subscribe to mouse events (clicks, scroll, etc.) */
42
+ onMouse(handler: (mouse: MouseMsg) => void): () => void
43
+ /** Subscribe to paste events (bracketed paste mode). */
44
+ onPaste?(handler: (paste: PasteMsg) => void): () => void
45
+ /** Subscribe to resize events */
46
+ onResize(handler: (width: number, height: number) => void): () => void
47
+ /** Stop the renderer */
48
+ stop(): void
49
+ /** Manually trigger one render frame (only in manualMode) */
50
+ renderNow(): void
51
+ /**
52
+ * Get the current screen as ANSI string (for screenshots/debugging).
53
+ * Returns the last rendered frame with all ANSI color codes intact.
54
+ */
55
+ getScreenshot(): string
56
+ /** Dispatch a key event (for remote control) */
57
+ dispatchKey?(key: KeyMsg): void
58
+ /** Dispatch a paste event (for remote control) */
59
+ dispatchPaste?(text: string): void
60
+ /** Dispatch a resize event (for remote control) */
61
+ dispatchResize?(width: number, height: number): void
62
+ }
63
+
64
+ /** Internal renderer type with container reference */
65
+ export interface TuiRendererInternal extends TuiRenderer {
66
+ _container: Container | null
67
+ }
68
+
69
+ export interface Container {
70
+ root: HostInstance | null
71
+ ctx: HostContext
72
+ /** @internal Static content root node (for Static component) */
73
+ staticRoot?: HostInstance | null
74
+ /** @internal Flag indicating static content needs flushing */
75
+ staticDirty?: boolean
76
+ }
77
+
78
+ export interface RendererOptions {
79
+ fps?: number
80
+ stdout?: NodeJS.WriteStream | TuiWriteStream
81
+ stdin?: NodeJS.ReadStream | TuiReadStream
82
+ /** Render mode: "fullscreen" uses alternate buffer, "inline" renders in-place */
83
+ mode?: "fullscreen" | "inline"
84
+ /** Exit the process on Ctrl+C unless preventDefault was called. Defaults to true. */
85
+ exitOnCtrlC?: boolean
86
+ /** Handle SIGINT/SIGTERM and process exit cleanup. Defaults to true. */
87
+ handleSignals?: boolean
88
+ /** Exit the process after handling SIGINT/SIGTERM. Defaults to true. */
89
+ exitOnSignal?: boolean
90
+ /** Override exit codes for signals. Defaults: SIGINT=130, SIGTERM=143. */
91
+ signalExitCodes?: Partial<Record<"SIGINT" | "SIGTERM", number>>
92
+ /** Enable diffed rendering (per-line). Defaults to true in runtime, false in manualMode (tests). */
93
+ diff?: boolean
94
+ /** Skip automatic render loop. Call renderNow() manually to render frames. */
95
+ manualMode?: boolean
96
+ /** Skip fullscreen/raw mode setup (for testing) */
97
+ skipTerminalSetup?: boolean
98
+ /** Enable bracketed paste (default true). */
99
+ enablePaste?: boolean
100
+ /** Enable mouse mode for scroll wheel events (default true in fullscreen). */
101
+ enableMouse?: boolean
102
+ /** Enable Kitty/xterm keyboard protocols for enhanced modifier detection (default true). */
103
+ enableKittyKeyboard?: boolean
104
+ /** Optional per-frame diagnostics hook. Called after each frame is written. */
105
+ debug?: {
106
+ onFrame?: (stats: FrameStats) => void
107
+ }
108
+ }
109
+
110
+ export interface FrameStats {
111
+ mode: "fullscreen" | "inline"
112
+ width: number
113
+ height: number
114
+ contentHeight: number
115
+ bytes: number
116
+ frameMs: number
117
+ phases: {
118
+ clear: number
119
+ layout: number
120
+ render: number
121
+ diffMs: number
122
+ write: number
123
+ }
124
+ timestamp: number
125
+ }
@@ -1,27 +1 @@
1
- import { createContext, useContext, useEffect, useState } from "react"
2
- import type { TuiRenderer } from "./renderer-types.js"
3
-
4
- // Context for accessing renderer in components
5
- export const RendererContext = createContext<TuiRenderer | null>(null)
6
-
7
- export function useRenderer(): TuiRenderer {
8
- const renderer = useContext(RendererContext)
9
- if (!renderer) {
10
- throw new Error("useRenderer must be used within a TUI renderer")
11
- }
12
- return renderer
13
- }
14
-
15
- /** Hook that returns terminal size and re-renders on resize */
16
- export function useTerminalSize(): { width: number; height: number } {
17
- const renderer = useRenderer()
18
- const [size, setSize] = useState({ width: renderer.width, height: renderer.height })
19
-
20
- useEffect(() => {
21
- return renderer.onResize((width, height) => {
22
- setSize({ width, height })
23
- })
24
- }, [renderer])
25
-
26
- return size
27
- }
1
+ export { RendererContext, useRenderer, useTerminalSize } from "./internal/renderer/context.js"
@@ -1,123 +1,10 @@
1
- import type { KeyMsg, MouseMsg } from "@effect-tui/core"
2
- import type { HostContext, HostInstance } from "./reconciler/types.js"
3
-
4
- export type PasteMsg = {
5
- type: "paste"
6
- text: string
7
- /** Set by renderer to allow handlers to stop further propagation. */
8
- defaultPrevented?: boolean
9
- preventDefault?: () => void
10
- }
11
-
12
- /** Minimal write stream interface for renderer output */
13
- export interface TuiWriteStream {
14
- write(s: string): void
15
- columns: number
16
- rows: number
17
- on(event: string, cb: () => void): void
18
- removeListener(event: string, cb: () => void): void
19
- }
20
-
21
- /** Minimal read stream interface for renderer input */
22
- export interface TuiReadStream {
23
- isTTY?: boolean
24
- setRawMode?(mode: boolean): void
25
- resume?(): void
26
- on(event: string, cb: (data: Buffer) => void): void
27
- removeListener(event: string, cb: (data: Buffer) => void): void
28
- }
29
-
30
- export interface TuiRenderer {
31
- /** Terminal width */
32
- width: number
33
- /** Terminal height */
34
- height: number
35
- /** Request a re-render */
36
- requestRender(): void
37
- /** Subscribe to per-frame stats (if enabled). */
38
- onFrameStats?(handler: (stats: FrameStats) => void): () => void
39
- /** Subscribe to keyboard events */
40
- onKey(handler: (key: KeyMsg) => void): () => void
41
- /** Subscribe to mouse events (clicks, scroll, etc.) */
42
- onMouse(handler: (mouse: MouseMsg) => void): () => void
43
- /** Subscribe to paste events (bracketed paste mode). */
44
- onPaste?(handler: (paste: PasteMsg) => void): () => void
45
- /** Subscribe to resize events */
46
- onResize(handler: (width: number, height: number) => void): () => void
47
- /** Stop the renderer */
48
- stop(): void
49
- /** Manually trigger one render frame (only in manualMode) */
50
- flush(): void
51
- /**
52
- * Get the current screen as ANSI string (for screenshots/debugging).
53
- * Returns the last rendered frame with all ANSI color codes intact.
54
- */
55
- getScreenshot(): string
56
- /** Dispatch a key event (for remote control) */
57
- dispatchKey?(key: KeyMsg): void
58
- /** Dispatch a paste event (for remote control) */
59
- dispatchPaste?(text: string): void
60
- /** Dispatch a resize event (for remote control) */
61
- dispatchResize?(width: number, height: number): void
62
- }
63
-
64
- /** Internal renderer type with container reference */
65
- export interface TuiRendererInternal extends TuiRenderer {
66
- _container: Container | null
67
- }
68
-
69
- export interface Container {
70
- root: HostInstance | null
71
- ctx: HostContext
72
- /** @internal Static content root node (for Static component) */
73
- staticRoot?: HostInstance | null
74
- /** @internal Flag indicating static content needs flushing */
75
- staticDirty?: boolean
76
- }
77
-
78
- export interface RendererOptions {
79
- fps?: number
80
- stdout?: NodeJS.WriteStream | TuiWriteStream
81
- stdin?: NodeJS.ReadStream | TuiReadStream
82
- /** Render mode: "fullscreen" uses alternate buffer, "inline" renders in-place */
83
- mode?: "fullscreen" | "inline"
84
- /** Exit the process on Ctrl+C unless preventDefault was called. Defaults to true. */
85
- exitOnCtrlC?: boolean
86
- /** Handle SIGINT/SIGTERM and process exit cleanup. Defaults to true. */
87
- handleSignals?: boolean
88
- /** Exit the process after handling SIGINT/SIGTERM. Defaults to true. */
89
- exitOnSignal?: boolean
90
- /** Override exit codes for signals. Defaults: SIGINT=130, SIGTERM=143. */
91
- signalExitCodes?: Partial<Record<"SIGINT" | "SIGTERM", number>>
92
- /** Enable diffed rendering (per-line). Defaults to true in runtime, false in manualMode (tests). */
93
- diff?: boolean
94
- /** Skip automatic render loop. Call flush() manually to render frames. */
95
- manualMode?: boolean
96
- /** Skip fullscreen/raw mode setup (for testing) */
97
- skipTerminalSetup?: boolean
98
- /** Enable bracketed paste (default true). */
99
- enablePaste?: boolean
100
- /** Enable mouse mode for scroll wheel events (default true in fullscreen). */
101
- enableMouse?: boolean
102
- /** Optional per-frame diagnostics hook. Called after each frame is written. */
103
- debug?: {
104
- onFrame?: (stats: FrameStats) => void
105
- }
106
- }
107
-
108
- export interface FrameStats {
109
- mode: "fullscreen" | "inline"
110
- width: number
111
- height: number
112
- contentHeight: number
113
- bytes: number
114
- frameMs: number
115
- phases: {
116
- clear: number
117
- layout: number
118
- render: number
119
- diffAnsi: number
120
- write: number
121
- }
122
- timestamp: number
123
- }
1
+ export type {
2
+ Container,
3
+ FrameStats,
4
+ PasteMsg,
5
+ RendererOptions,
6
+ TuiReadStream,
7
+ TuiRenderer,
8
+ TuiRendererInternal,
9
+ TuiWriteStream,
10
+ } from "./internal/renderer/types.js"