@effect-tui/react 0.1.7 → 0.2.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 (295) hide show
  1. package/dist/jsx-runtime.d.ts +6 -6
  2. package/dist/jsx-runtime.d.ts.map +1 -1
  3. package/dist/src/codeblock.js.map +1 -1
  4. package/dist/src/components/Divider.d.ts.map +1 -1
  5. package/dist/src/components/Divider.js +1 -1
  6. package/dist/src/components/Divider.js.map +1 -1
  7. package/dist/src/components/Markdown.d.ts.map +1 -1
  8. package/dist/src/components/Markdown.js +1 -1
  9. package/dist/src/components/Markdown.js.map +1 -1
  10. package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
  11. package/dist/src/components/MultilineTextInput.js +2 -2
  12. package/dist/src/components/MultilineTextInput.js.map +1 -1
  13. package/dist/src/components/Static.d.ts.map +1 -1
  14. package/dist/src/components/Static.js +1 -1
  15. package/dist/src/components/Static.js.map +1 -1
  16. package/dist/src/components/TextInput.d.ts.map +1 -1
  17. package/dist/src/components/TextInput.js +2 -2
  18. package/dist/src/components/TextInput.js.map +1 -1
  19. package/dist/src/components/index.d.ts +4 -4
  20. package/dist/src/components/index.d.ts.map +1 -1
  21. package/dist/src/components/index.js +4 -4
  22. package/dist/src/components/index.js.map +1 -1
  23. package/dist/src/console/ConsoleCapture.d.ts.map +1 -1
  24. package/dist/src/console/ConsoleCapture.js +1 -1
  25. package/dist/src/console/ConsoleCapture.js.map +1 -1
  26. package/dist/src/console/ConsolePopover.d.ts.map +1 -1
  27. package/dist/src/console/ConsolePopover.js +5 -7
  28. package/dist/src/console/ConsolePopover.js.map +1 -1
  29. package/dist/src/console/index.d.ts +1 -1
  30. package/dist/src/console/index.d.ts.map +1 -1
  31. package/dist/src/console/index.js +1 -1
  32. package/dist/src/console/index.js.map +1 -1
  33. package/dist/src/console/useConsole.d.ts.map +1 -1
  34. package/dist/src/console/useConsole.js +2 -2
  35. package/dist/src/console/useConsole.js.map +1 -1
  36. package/dist/src/debug/DebugOverlay.d.ts +1 -1
  37. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  38. package/dist/src/debug/DebugOverlay.js +1 -1
  39. package/dist/src/debug/DebugOverlay.js.map +1 -1
  40. package/dist/src/dev/Toast.d.ts.map +1 -1
  41. package/dist/src/dev/Toast.js +2 -2
  42. package/dist/src/dev/Toast.js.map +1 -1
  43. package/dist/src/dev/index.d.ts +1 -1
  44. package/dist/src/dev/index.d.ts.map +1 -1
  45. package/dist/src/dev/index.js +1 -1
  46. package/dist/src/dev/index.js.map +1 -1
  47. package/dist/src/dev.d.ts +1 -2
  48. package/dist/src/dev.d.ts.map +1 -1
  49. package/dist/src/dev.js +10 -10
  50. package/dist/src/dev.js.map +1 -1
  51. package/dist/src/highlight.d.ts +1 -1
  52. package/dist/src/highlight.d.ts.map +1 -1
  53. package/dist/src/highlight.js.map +1 -1
  54. package/dist/src/hooks/index.d.ts +4 -3
  55. package/dist/src/hooks/index.d.ts.map +1 -1
  56. package/dist/src/hooks/index.js +1 -0
  57. package/dist/src/hooks/index.js.map +1 -1
  58. package/dist/src/hooks/use-keyboard.d.ts.map +1 -1
  59. package/dist/src/hooks/use-keyboard.js.map +1 -1
  60. package/dist/src/hooks/use-mouse.d.ts +1 -1
  61. package/dist/src/hooks/use-mouse.d.ts.map +1 -1
  62. package/dist/src/hooks/use-mouse.js.map +1 -1
  63. package/dist/src/hooks/use-quit.d.ts +21 -0
  64. package/dist/src/hooks/use-quit.d.ts.map +1 -0
  65. package/dist/src/hooks/use-quit.js +29 -0
  66. package/dist/src/hooks/use-quit.js.map +1 -0
  67. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  68. package/dist/src/hooks/use-scroll.js +2 -2
  69. package/dist/src/hooks/use-scroll.js.map +1 -1
  70. package/dist/src/hooks/useFrameStats.d.ts.map +1 -1
  71. package/dist/src/hooks/useFrameStats.js.map +1 -1
  72. package/dist/src/hosts/base.d.ts +3 -4
  73. package/dist/src/hosts/base.d.ts.map +1 -1
  74. package/dist/src/hosts/base.js +42 -6
  75. package/dist/src/hosts/base.js.map +1 -1
  76. package/dist/src/hosts/box.d.ts +3 -3
  77. package/dist/src/hosts/box.d.ts.map +1 -1
  78. package/dist/src/hosts/box.js +1 -1
  79. package/dist/src/hosts/box.js.map +1 -1
  80. package/dist/src/hosts/canvas.d.ts +3 -3
  81. package/dist/src/hosts/canvas.d.ts.map +1 -1
  82. package/dist/src/hosts/canvas.js +1 -1
  83. package/dist/src/hosts/canvas.js.map +1 -1
  84. package/dist/src/hosts/codeblock.d.ts +2 -2
  85. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  86. package/dist/src/hosts/codeblock.js +1 -1
  87. package/dist/src/hosts/codeblock.js.map +1 -1
  88. package/dist/src/hosts/flex-container.d.ts +3 -3
  89. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  90. package/dist/src/hosts/flex-container.js +5 -2
  91. package/dist/src/hosts/flex-container.js.map +1 -1
  92. package/dist/src/hosts/hstack.d.ts +1 -1
  93. package/dist/src/hosts/index.d.ts +8 -8
  94. package/dist/src/hosts/index.d.ts.map +1 -1
  95. package/dist/src/hosts/index.js +13 -13
  96. package/dist/src/hosts/index.js.map +1 -1
  97. package/dist/src/hosts/overlay-item.d.ts +1 -1
  98. package/dist/src/hosts/overlay.d.ts +1 -1
  99. package/dist/src/hosts/overlay.d.ts.map +1 -1
  100. package/dist/src/hosts/overlay.js +1 -1
  101. package/dist/src/hosts/overlay.js.map +1 -1
  102. package/dist/src/hosts/scroll.d.ts +3 -2
  103. package/dist/src/hosts/scroll.d.ts.map +1 -1
  104. package/dist/src/hosts/scroll.js +12 -3
  105. package/dist/src/hosts/scroll.js.map +1 -1
  106. package/dist/src/hosts/single-child.d.ts +1 -1
  107. package/dist/src/hosts/single-child.d.ts.map +1 -1
  108. package/dist/src/hosts/spacer.d.ts +2 -1
  109. package/dist/src/hosts/spacer.d.ts.map +1 -1
  110. package/dist/src/hosts/spacer.js +10 -9
  111. package/dist/src/hosts/spacer.js.map +1 -1
  112. package/dist/src/hosts/text.d.ts +2 -2
  113. package/dist/src/hosts/text.d.ts.map +1 -1
  114. package/dist/src/hosts/text.js +1 -1
  115. package/dist/src/hosts/text.js.map +1 -1
  116. package/dist/src/hosts/vstack.d.ts +1 -1
  117. package/dist/src/hosts/zstack.d.ts +2 -2
  118. package/dist/src/hosts/zstack.d.ts.map +1 -1
  119. package/dist/src/hosts/zstack.js +1 -1
  120. package/dist/src/hosts/zstack.js.map +1 -1
  121. package/dist/src/index.d.ts +17 -21
  122. package/dist/src/index.d.ts.map +1 -1
  123. package/dist/src/index.js +11 -15
  124. package/dist/src/index.js.map +1 -1
  125. package/dist/src/inline/index.d.ts.map +1 -1
  126. package/dist/src/inline/index.js +1 -1
  127. package/dist/src/inline/index.js.map +1 -1
  128. package/dist/src/motion/color-motion-value.d.ts +1 -1
  129. package/dist/src/motion/color-motion-value.d.ts.map +1 -1
  130. package/dist/src/motion/color-motion-value.js.map +1 -1
  131. package/dist/src/motion/color.d.ts +1 -1
  132. package/dist/src/motion/color.d.ts.map +1 -1
  133. package/dist/src/motion/color.js +1 -1
  134. package/dist/src/motion/color.js.map +1 -1
  135. package/dist/src/motion/color.test.js +2 -2
  136. package/dist/src/motion/color.test.js.map +1 -1
  137. package/dist/src/motion/hooks.d.ts +3 -3
  138. package/dist/src/motion/hooks.d.ts.map +1 -1
  139. package/dist/src/motion/hooks.js +1 -1
  140. package/dist/src/motion/hooks.js.map +1 -1
  141. package/dist/src/motion/index.d.ts +2 -2
  142. package/dist/src/motion/index.d.ts.map +1 -1
  143. package/dist/src/motion/index.js +3 -3
  144. package/dist/src/motion/index.js.map +1 -1
  145. package/dist/src/motion/motion-value.d.ts +5 -5
  146. package/dist/src/motion/motion-value.d.ts.map +1 -1
  147. package/dist/src/motion/motion-value.js +4 -4
  148. package/dist/src/motion/motion-value.js.map +1 -1
  149. package/dist/src/motion/motion-value.test.js +1 -1
  150. package/dist/src/motion/motion-value.test.js.map +1 -1
  151. package/dist/src/reconciler/host-config.d.ts +1 -1
  152. package/dist/src/reconciler/host-config.d.ts.map +1 -1
  153. package/dist/src/reconciler/host-config.js +1 -1
  154. package/dist/src/reconciler/host-config.js.map +1 -1
  155. package/dist/src/reconciler/types.d.ts +10 -5
  156. package/dist/src/reconciler/types.d.ts.map +1 -1
  157. package/dist/src/remote/Procedures.d.ts.map +1 -1
  158. package/dist/src/remote/Procedures.js.map +1 -1
  159. package/dist/src/remote/Router.d.ts +1 -1
  160. package/dist/src/remote/Router.d.ts.map +1 -1
  161. package/dist/src/remote/Router.js +1 -1
  162. package/dist/src/remote/Router.js.map +1 -1
  163. package/dist/src/remote/Server.d.ts +1 -1
  164. package/dist/src/remote/Server.d.ts.map +1 -1
  165. package/dist/src/remote/Server.js +3 -3
  166. package/dist/src/remote/Server.js.map +1 -1
  167. package/dist/src/remote/index.d.ts +2 -2
  168. package/dist/src/remote/index.d.ts.map +1 -1
  169. package/dist/src/remote/index.js +5 -5
  170. package/dist/src/remote/index.js.map +1 -1
  171. package/dist/src/renderer/core/FrameBuilder.d.ts.map +1 -1
  172. package/dist/src/renderer/core/FrameBuilder.js.map +1 -1
  173. package/dist/src/renderer/input/InputProcessor.d.ts +1 -0
  174. package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -1
  175. package/dist/src/renderer/input/InputProcessor.js +7 -2
  176. package/dist/src/renderer/input/InputProcessor.js.map +1 -1
  177. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts +2 -1
  178. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts.map +1 -1
  179. package/dist/src/renderer/lifecycle/TerminalSetup.js +26 -17
  180. package/dist/src/renderer/lifecycle/TerminalSetup.js.map +1 -1
  181. package/dist/src/renderer/lifecycle/index.d.ts +1 -1
  182. package/dist/src/renderer/lifecycle/index.d.ts.map +1 -1
  183. package/dist/src/renderer/lifecycle/index.js +1 -1
  184. package/dist/src/renderer/lifecycle/index.js.map +1 -1
  185. package/dist/src/renderer/modes/FullscreenRenderer.d.ts +1 -1
  186. package/dist/src/renderer/modes/FullscreenRenderer.d.ts.map +1 -1
  187. package/dist/src/renderer/modes/InlineRenderer.d.ts +1 -1
  188. package/dist/src/renderer/modes/InlineRenderer.d.ts.map +1 -1
  189. package/dist/src/renderer/modes/InlineRenderer.js +1 -1
  190. package/dist/src/renderer/modes/InlineRenderer.js.map +1 -1
  191. package/dist/src/renderer/modes/StaticContentRenderer.d.ts.map +1 -1
  192. package/dist/src/renderer/modes/StaticContentRenderer.js.map +1 -1
  193. package/dist/src/renderer/modes/index.d.ts +1 -1
  194. package/dist/src/renderer/modes/index.d.ts.map +1 -1
  195. package/dist/src/renderer/modes/index.js.map +1 -1
  196. package/dist/src/renderer-context.js +1 -1
  197. package/dist/src/renderer-context.js.map +1 -1
  198. package/dist/src/renderer-types.d.ts +1 -1
  199. package/dist/src/renderer-types.d.ts.map +1 -1
  200. package/dist/src/renderer.d.ts +4 -2
  201. package/dist/src/renderer.d.ts.map +1 -1
  202. package/dist/src/renderer.js +45 -13
  203. package/dist/src/renderer.js.map +1 -1
  204. package/dist/src/test/index.d.ts +2 -2
  205. package/dist/src/test/index.d.ts.map +1 -1
  206. package/dist/src/test/index.js +1 -1
  207. package/dist/src/test/index.js.map +1 -1
  208. package/dist/src/test/render-tui.d.ts +7 -2
  209. package/dist/src/test/render-tui.d.ts.map +1 -1
  210. package/dist/src/test/render-tui.js +5 -2
  211. package/dist/src/test/render-tui.js.map +1 -1
  212. package/dist/src/utils/flex-layout.d.ts +1 -1
  213. package/dist/src/utils/flex-layout.d.ts.map +1 -1
  214. package/dist/src/utils/flex-layout.js +21 -7
  215. package/dist/src/utils/flex-layout.js.map +1 -1
  216. package/dist/src/utils/index.d.ts +4 -4
  217. package/dist/src/utils/index.d.ts.map +1 -1
  218. package/dist/src/utils/index.js +2 -2
  219. package/dist/src/utils/index.js.map +1 -1
  220. package/dist/src/utils/styles.d.ts.map +1 -1
  221. package/dist/src/utils/styles.js.map +1 -1
  222. package/dist/src/visualize/index.js +2 -2
  223. package/dist/src/visualize/index.js.map +1 -1
  224. package/dist/tsconfig.tsbuildinfo +1 -1
  225. package/jsx-runtime.ts +6 -6
  226. package/package.json +3 -2
  227. package/src/codeblock.tsx +1 -1
  228. package/src/components/Divider.tsx +1 -1
  229. package/src/components/Markdown.tsx +2 -2
  230. package/src/components/MultilineTextInput.tsx +9 -9
  231. package/src/components/Static.tsx +1 -1
  232. package/src/components/TextInput.tsx +8 -8
  233. package/src/components/index.ts +4 -4
  234. package/src/console/ConsoleCapture.ts +1 -1
  235. package/src/console/ConsolePopover.tsx +27 -26
  236. package/src/console/index.ts +1 -3
  237. package/src/console/useConsole.ts +2 -2
  238. package/src/debug/DebugOverlay.ts +3 -6
  239. package/src/dev/Toast.tsx +12 -10
  240. package/src/dev/index.ts +1 -1
  241. package/src/dev.tsx +11 -12
  242. package/src/highlight.ts +1 -1
  243. package/src/hooks/index.ts +4 -3
  244. package/src/hooks/use-keyboard.ts +1 -1
  245. package/src/hooks/use-mouse.ts +1 -1
  246. package/src/hooks/use-quit.ts +33 -0
  247. package/src/hooks/use-scroll.ts +9 -11
  248. package/src/hooks/useFrameStats.ts +1 -1
  249. package/src/hosts/base.ts +38 -8
  250. package/src/hosts/box.ts +4 -4
  251. package/src/hosts/canvas.ts +3 -3
  252. package/src/hosts/codeblock.ts +3 -3
  253. package/src/hosts/flex-container.ts +6 -4
  254. package/src/hosts/hstack.ts +1 -1
  255. package/src/hosts/index.ts +14 -14
  256. package/src/hosts/overlay-item.ts +1 -1
  257. package/src/hosts/overlay.ts +2 -2
  258. package/src/hosts/scroll.ts +16 -5
  259. package/src/hosts/single-child.ts +1 -1
  260. package/src/hosts/spacer.ts +12 -10
  261. package/src/hosts/text.ts +3 -3
  262. package/src/hosts/vstack.ts +1 -1
  263. package/src/hosts/zstack.ts +2 -2
  264. package/src/index.ts +60 -60
  265. package/src/inline/index.tsx +1 -1
  266. package/src/motion/color-motion-value.ts +1 -1
  267. package/src/motion/color.test.ts +2 -2
  268. package/src/motion/color.ts +2 -2
  269. package/src/motion/hooks.ts +3 -3
  270. package/src/motion/index.ts +8 -8
  271. package/src/motion/motion-value.test.ts +1 -1
  272. package/src/motion/motion-value.ts +11 -11
  273. package/src/reconciler/host-config.ts +3 -3
  274. package/src/reconciler/types.ts +10 -5
  275. package/src/remote/Procedures.ts +1 -7
  276. package/src/remote/Router.ts +3 -7
  277. package/src/remote/Server.ts +7 -12
  278. package/src/remote/index.ts +6 -10
  279. package/src/renderer/core/FrameBuilder.ts +1 -1
  280. package/src/renderer/input/InputProcessor.ts +7 -2
  281. package/src/renderer/lifecycle/TerminalSetup.ts +29 -18
  282. package/src/renderer/lifecycle/index.ts +1 -1
  283. package/src/renderer/modes/FullscreenRenderer.ts +1 -1
  284. package/src/renderer/modes/InlineRenderer.ts +2 -2
  285. package/src/renderer/modes/StaticContentRenderer.ts +1 -1
  286. package/src/renderer/modes/index.ts +1 -1
  287. package/src/renderer-context.ts +1 -1
  288. package/src/renderer-types.ts +1 -1
  289. package/src/renderer.ts +62 -25
  290. package/src/test/index.ts +4 -4
  291. package/src/test/render-tui.ts +10 -3
  292. package/src/utils/flex-layout.ts +21 -8
  293. package/src/utils/index.ts +8 -8
  294. package/src/utils/styles.ts +2 -2
  295. package/src/visualize/index.tsx +2 -2
@@ -8,14 +8,14 @@
8
8
  // 2. Connect using etui CLI or raw socket at /tmp/effect-tui-sessions/<pid>.sock
9
9
 
10
10
  export { TuiRpcs } from "./Procedures.js"
11
- export { TuiSession, HandlersLive } from "./Router.js"
12
- export { makeServerLayer, getSocketPath } from "./Server.js"
11
+ export { HandlersLive, TuiSession } from "./Router.js"
12
+ export { getSocketPath, makeServerLayer } from "./Server.js"
13
13
 
14
- import { Effect, Exit, Layer, Scope } from "effect"
15
14
  import * as fs from "node:fs"
15
+ import { Effect, Exit, Layer, Scope } from "effect"
16
16
  import type { TuiRenderer } from "../renderer-types.js"
17
17
  import type { TuiSessionImpl } from "./Router.js"
18
- import { makeServerLayer, getSocketPath } from "./Server.js"
18
+ import { getSocketPath, makeServerLayer } from "./Server.js"
19
19
 
20
20
  export interface EnableRemoteOptions {
21
21
  /** Custom socket path (defaults to /tmp/effect-tui-sessions/<pid>.sock) */
@@ -55,13 +55,9 @@ function deriveSessionName(entryPath: string): string {
55
55
  * @param options - Socket path and entry path for session identification
56
56
  * @returns A cleanup function to stop the server
57
57
  */
58
- export function enableRemote(
59
- renderer: TuiRenderer,
60
- options?: EnableRemoteOptions | string,
61
- ): () => void {
58
+ export function enableRemote(renderer: TuiRenderer, options?: EnableRemoteOptions | string): () => void {
62
59
  // Support old API: enableRemote(renderer, socketPath?)
63
- const opts: EnableRemoteOptions =
64
- typeof options === "string" ? { socketPath: options } : options ?? {}
60
+ const opts: EnableRemoteOptions = typeof options === "string" ? { socketPath: options } : (options ?? {})
65
61
  const actualPath = opts.socketPath ?? getSocketPath()
66
62
 
67
63
  // Derive session name from entry path
@@ -1,7 +1,7 @@
1
1
  import { performance } from "node:perf_hooks"
2
2
  import type { CellBuffer, Palette } from "@effect-tui/core"
3
- import type { HostInstance } from "../../reconciler/types.js"
4
3
  import * as Prof from "../../profiler.js"
4
+ import type { HostInstance } from "../../reconciler/types.js"
5
5
 
6
6
  export interface FrameTimings {
7
7
  clear: number
@@ -1,4 +1,4 @@
1
- import { decodeInput, ANSI, type KeyMsg, type MouseMsg } from "@effect-tui/core"
1
+ import { ANSI, decodeInput, type KeyMsg, type MouseMsg } from "@effect-tui/core"
2
2
 
3
3
  export interface InputProcessorConfig {
4
4
  exitOnCtrlC: boolean
@@ -7,6 +7,7 @@ export interface InputProcessorConfig {
7
7
  dispatchPaste: (text: string) => void
8
8
  flushSync: <T>(fn: () => T) => T
9
9
  onInputProcessed: () => void
10
+ onQuit?: () => void // Called instead of process.exit() for proper cleanup
10
11
  }
11
12
 
12
13
  /**
@@ -86,7 +87,11 @@ export class InputProcessor {
86
87
  // Default Ctrl+C handling - exit unless user called preventDefault()
87
88
  // Only match Ctrl+C (not Ctrl+Shift+C which should be available for copy)
88
89
  if (this.config.exitOnCtrlC && !wrapped.defaultPrevented && event.ctrl && !event.shift && event.text === "c") {
89
- process.exit(0)
90
+ if (this.config.onQuit) {
91
+ this.config.onQuit()
92
+ } else {
93
+ process.exit(0)
94
+ }
90
95
  }
91
96
  }
92
97
  }
@@ -1,5 +1,5 @@
1
1
  import { ANSI, Terminal } from "@effect-tui/core"
2
- import type { TuiWriteStream, TuiReadStream } from "../../renderer-types.js"
2
+ import type { TuiReadStream, TuiWriteStream } from "../../renderer-types.js"
3
3
 
4
4
  export interface TerminalSetupConfig {
5
5
  mode: "fullscreen" | "inline"
@@ -70,36 +70,47 @@ export class TerminalSetup {
70
70
  /**
71
71
  * Restore terminal to normal state.
72
72
  * Call this when stopping the renderer.
73
+ * Uses a single write call to ensure atomic output and reliable flushing.
73
74
  */
74
75
  teardown(): void {
75
76
  if (this.config.skipTerminalSetup) return
76
77
 
77
- if (this.config.mode === "fullscreen") {
78
- this.stdout.write(Terminal.exitFullscreen)
79
- } else {
80
- // Re-enable reflow on exit
81
- this.stdout.write(ANSI.reflow.enable)
82
- this.stdout.write("\r\n")
78
+ // Disable raw mode FIRST - this is critical for proper terminal restoration
79
+ // Must happen before writing escape sequences so the terminal processes them correctly
80
+ if (this.stdin.isTTY && this.stdin.setRawMode) {
81
+ this.stdin.setRawMode(false)
83
82
  }
84
83
 
85
- if (this.config.enablePaste) {
86
- this.stdout.write(ANSI.paste.disable)
84
+ // Build all escape sequences into a single string for atomic write
85
+ // This ensures all sequences are flushed together before process exit
86
+ let output = ""
87
+
88
+ // Disable enhanced keyboard protocols first
89
+ if (this.config.enableKittyKeyboard !== false) {
90
+ output += ANSI.keyboard.disable
91
+ output += ANSI.modifyOtherKeys.disable
87
92
  }
88
93
 
89
94
  if (this.config.enableMouse) {
90
- this.stdout.write(ANSI.mouse.disable)
95
+ output += ANSI.mouse.disable
91
96
  }
92
97
 
93
- // Disable enhanced keyboard protocols
94
- if (this.config.enableKittyKeyboard !== false) {
95
- this.stdout.write(ANSI.keyboard.disable)
96
- this.stdout.write(ANSI.modifyOtherKeys.disable)
98
+ if (this.config.enablePaste) {
99
+ output += ANSI.paste.disable
97
100
  }
98
101
 
99
- this.stdout.write(Terminal.showCursor)
100
-
101
- if (this.stdin.isTTY && this.stdin.setRawMode) {
102
- this.stdin.setRawMode(false)
102
+ // Exit fullscreen or re-enable reflow
103
+ if (this.config.mode === "fullscreen") {
104
+ output += Terminal.exitFullscreen
105
+ } else {
106
+ output += ANSI.reflow.enable
107
+ output += "\r\n"
103
108
  }
109
+
110
+ // Always show cursor at the end
111
+ output += Terminal.showCursor
112
+
113
+ // Single atomic write - more reliable for process exit scenarios
114
+ this.stdout.write(output)
104
115
  }
105
116
  }
@@ -1,3 +1,3 @@
1
1
  export { EventBus } from "./EventBus.js"
2
+ export { ResizeManager, type ResizeResult, type ResizeState } from "./ResizeManager.js"
2
3
  export { TerminalSetup, type TerminalSetupConfig } from "./TerminalSetup.js"
3
- export { ResizeManager, type ResizeState, type ResizeResult } from "./ResizeManager.js"
@@ -1,5 +1,5 @@
1
1
  import { ANSI, emitRowWithReset, rowChanged } from "@effect-tui/core"
2
- import type { RendererMode, RenderContext, RenderOutput } from "./RendererMode.js"
2
+ import type { RenderContext, RendererMode, RenderOutput } from "./RendererMode.js"
3
3
 
4
4
  /**
5
5
  * Fullscreen rendering mode using alternate buffer.
@@ -1,5 +1,5 @@
1
- import { ANSI, emitRowWithReset, rowContentWidth, findChangeWindow } from "@effect-tui/core"
2
- import type { RendererMode, RenderContext, RenderOutput } from "./RendererMode.js"
1
+ import { ANSI, emitRowWithReset, findChangeWindow, rowContentWidth } from "@effect-tui/core"
2
+ import type { RenderContext, RendererMode, RenderOutput } from "./RendererMode.js"
3
3
 
4
4
  /**
5
5
  * Inline rendering mode that renders in-place without alternate buffer.
@@ -1,4 +1,4 @@
1
- import { CellBuffer, emitRowWithReset, rowContentWidth, type Palette } from "@effect-tui/core"
1
+ import { CellBuffer, emitRowWithReset, type Palette, rowContentWidth } from "@effect-tui/core"
2
2
  import type { HostInstance } from "../../reconciler/types.js"
3
3
  import type { TuiWriteStream } from "../../renderer-types.js"
4
4
 
@@ -1,4 +1,4 @@
1
- export type { RendererMode, RenderContext, RenderOutput } from "./RendererMode.js"
2
1
  export { FullscreenRenderer } from "./FullscreenRenderer.js"
3
2
  export { InlineRenderer } from "./InlineRenderer.js"
3
+ export type { RenderContext, RendererMode, RenderOutput } from "./RendererMode.js"
4
4
  export { StaticContentRenderer } from "./StaticContentRenderer.js"
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useState, useEffect } from "react"
1
+ import { createContext, useContext, useEffect, useState } from "react"
2
2
  import type { TuiRenderer } from "./renderer-types.js"
3
3
 
4
4
  // Context for accessing renderer in components
@@ -1,5 +1,5 @@
1
1
  import type { KeyMsg, MouseMsg } from "@effect-tui/core"
2
- import type { HostInstance, HostContext } from "./reconciler/types.js"
2
+ import type { HostContext, HostInstance } from "./reconciler/types.js"
3
3
 
4
4
  /** Minimal write stream interface for renderer output */
5
5
  export interface TuiWriteStream {
package/src/renderer.ts CHANGED
@@ -1,30 +1,29 @@
1
- import React, { type ReactNode } from "react"
2
1
  import { performance } from "node:perf_hooks"
3
- import { ANSI, type KeyMsg, type MouseMsg, bufferToString } from "@effect-tui/core"
4
- import { reconciler, flushSync } from "./reconciler/host-config.js"
5
- import type { HostContext } from "./reconciler/types.js"
6
- import * as Prof from "./profiler.js"
2
+ import { ANSI, bufferToString, type KeyMsg, type MouseMsg } from "@effect-tui/core"
3
+ import React, { type ReactNode } from "react"
7
4
  import { DEFAULT_FPS } from "./constants.js"
5
+ import * as Prof from "./profiler.js"
6
+ import { flushSync, reconciler } from "./reconciler/host-config.js"
7
+ import type { HostContext } from "./reconciler/types.js"
8
+ // Extracted modules
9
+ import { FrameBuilder, RendererState } from "./renderer/core/index.js"
10
+ import { InputProcessor } from "./renderer/input/index.js"
11
+ import { EventBus, TerminalSetup } from "./renderer/lifecycle/index.js"
12
+ import { FullscreenRenderer, InlineRenderer, StaticContentRenderer } from "./renderer/modes/index.js"
13
+ import { RendererContext } from "./renderer-context.js"
8
14
  import type {
9
- TuiWriteStream,
15
+ Container,
16
+ FrameStats,
17
+ RendererOptions,
10
18
  TuiReadStream,
11
19
  TuiRenderer,
12
20
  TuiRendererInternal,
13
- Container,
14
- RendererOptions,
15
- FrameStats,
21
+ TuiWriteStream,
16
22
  } from "./renderer-types.js"
17
- import { RendererContext } from "./renderer-context.js"
18
-
19
- // Extracted modules
20
- import { RendererState, FrameBuilder } from "./renderer/core/index.js"
21
- import { InputProcessor } from "./renderer/input/index.js"
22
- import { EventBus, TerminalSetup } from "./renderer/lifecycle/index.js"
23
- import { FullscreenRenderer, InlineRenderer, StaticContentRenderer } from "./renderer/modes/index.js"
24
23
 
25
- // Re-export types and context for backwards compatibility
26
- export type { TuiWriteStream, TuiReadStream, TuiRenderer, RendererOptions, FrameStats } from "./renderer-types.js"
27
24
  export { RendererContext, useRenderer, useTerminalSize } from "./renderer-context.js"
25
+ // Re-export types and context for backwards compatibility
26
+ export type { FrameStats, RendererOptions, TuiReadStream, TuiRenderer, TuiWriteStream } from "./renderer-types.js"
28
27
 
29
28
  export function createRenderer(options?: RendererOptions): TuiRenderer {
30
29
  const fps = options?.fps ?? DEFAULT_FPS
@@ -71,6 +70,11 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
71
70
  onInputProcessed: () => {
72
71
  if (!manualMode) renderFrame()
73
72
  },
73
+ onQuit: () => {
74
+ // Clean up terminal state before exiting
75
+ renderer.stop()
76
+ process.exit(0)
77
+ },
74
78
  })
75
79
 
76
80
  // The render frame logic
@@ -162,7 +166,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
162
166
  })
163
167
 
164
168
  // Combine static + dynamic output for atomic write
165
- let output = staticOutput + modeOutput + state.palette.sgr(0)
169
+ const output = staticOutput + modeOutput + state.palette.sgr(0)
166
170
  contentH = contentHeight
167
171
  const diffAnsiMs = performance.now() - diffStartMs
168
172
  Prof.endPhase("diff+ansi", t)
@@ -300,6 +304,27 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
300
304
  }, frameMs)
301
305
  }
302
306
 
307
+ // Process exit handlers - ensure terminal is restored on any exit
308
+ // These handlers are critical for proper cleanup when process.exit() is called
309
+ let cleanedUp = false
310
+ const cleanup = () => {
311
+ if (cleanedUp) return
312
+ cleanedUp = true
313
+ renderer.stop()
314
+ }
315
+
316
+ // Handle normal process exit (synchronous - runs before exit completes)
317
+ const onExit = () => cleanup()
318
+ process.on("exit", onExit)
319
+
320
+ // Handle SIGINT (Ctrl+C from shell, not from TUI input) and SIGTERM
321
+ const onSignal = () => {
322
+ cleanup()
323
+ process.exit(0)
324
+ }
325
+ process.on("SIGINT", onSignal)
326
+ process.on("SIGTERM", onSignal)
327
+
303
328
  ;(renderer as TuiRendererInternal)._container = null
304
329
  return renderer
305
330
  }
@@ -365,6 +390,8 @@ export interface RenderInstance {
365
390
  rerender(element: ReactNode): void
366
391
  unmount(): void
367
392
  waitUntilExit(): Promise<void>
393
+ /** Cleanly exit the application, restoring terminal state before process.exit() */
394
+ quit(code?: number): void
368
395
  }
369
396
 
370
397
  export function render(element: ReactNode, options?: RendererOptions): RenderInstance {
@@ -383,18 +410,20 @@ export function render(element: ReactNode, options?: RendererOptions): RenderIns
383
410
  }
384
411
  })
385
412
 
413
+ // Resolve the exit promise on process exit
414
+ // Note: Terminal cleanup is handled by createRenderer's exit handlers
386
415
  const onExit = () => {
387
- if (!resolved) {
388
- renderer.stop()
389
- resolveExit?.()
390
- }
416
+ resolveExit?.()
391
417
  }
392
418
  process.once("exit", onExit)
393
419
 
394
420
  const unmount = () => {
395
421
  process.off("exit", onExit)
396
- renderer.stop()
397
- resolveExit?.()
422
+ if (!resolved) {
423
+ resolved = true
424
+ renderer.stop()
425
+ resolveExit?.()
426
+ }
398
427
  }
399
428
 
400
429
  const rerender = (next: ReactNode) => {
@@ -402,11 +431,19 @@ export function render(element: ReactNode, options?: RendererOptions): RenderIns
402
431
  root.render(next)
403
432
  }
404
433
 
434
+ // Clean quit function - renderer.stop() + process.exit()
435
+ // The createRenderer exit handler will also run, but it's idempotent
436
+ const quit = (code = 0) => {
437
+ renderer.stop()
438
+ process.exit(code)
439
+ }
440
+
405
441
  return {
406
442
  renderer,
407
443
  root,
408
444
  rerender,
409
445
  unmount,
410
446
  waitUntilExit: () => exitPromise,
447
+ quit,
411
448
  }
412
449
  }
package/src/test/index.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { renderTUI, type RenderTUIOptions, type RenderTUIResult } from "./render-tui.js"
2
1
  export {
3
- MockStdout,
4
- MockStdin,
5
2
  encodeKey,
6
- stripAnsi,
7
3
  getVisibleLines,
4
+ MockStdin,
5
+ MockStdout,
6
+ stripAnsi,
8
7
  } from "./mock-streams.js"
8
+ export { type RenderTUIOptions, type RenderTUIResult, renderTUI } from "./render-tui.js"
@@ -1,12 +1,14 @@
1
- import type { ReactElement } from "react"
2
1
  import type { KeyMsg } from "@effect-tui/core"
3
- import { createRenderer, createRoot } from "../renderer.js"
2
+ import type { ReactElement } from "react"
4
3
  import { flushPassiveEffects, flushSync } from "../reconciler/host-config.js"
5
- import { MockStdout, MockStdin, stripAnsi, getVisibleLines } from "./mock-streams.js"
4
+ import { createRenderer, createRoot } from "../renderer.js"
5
+ import { getVisibleLines, MockStdin, MockStdout, stripAnsi } from "./mock-streams.js"
6
6
 
7
7
  export interface RenderTUIOptions {
8
8
  width?: number
9
9
  height?: number
10
+ mode?: "fullscreen" | "inline"
11
+ diff?: boolean
10
12
  }
11
13
 
12
14
  export interface RenderTUIResult {
@@ -26,6 +28,8 @@ export interface RenderTUIResult {
26
28
  stdout: MockStdout
27
29
  /** Access mock stdin for advanced input simulation */
28
30
  stdin: MockStdin
31
+ /** Access renderer for advanced assertions */
32
+ renderer: ReturnType<typeof createRenderer>
29
33
  /** Resize the terminal */
30
34
  resize(width: number, height: number): void
31
35
  }
@@ -60,6 +64,8 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
60
64
  stdin: stdin as any,
61
65
  manualMode: true,
62
66
  skipTerminalSetup: true,
67
+ mode: options?.mode,
68
+ diff: options?.diff,
63
69
  })
64
70
 
65
71
  const root = createRoot(renderer)
@@ -106,6 +112,7 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
106
112
 
107
113
  stdout,
108
114
  stdin,
115
+ renderer,
109
116
 
110
117
  resize(w: number, h: number) {
111
118
  stdout.resize(w, h)
@@ -2,9 +2,9 @@
2
2
  * Generic flex layout algorithm for VStack and HStack.
3
3
  */
4
4
 
5
- import type { HostInstance, Rect, Size } from "../reconciler/types.js"
5
+ import { type Axis, crossDim, crossPos, crossSize, mainDim, mainPos, mainSize, makeRect } from "@effect-tui/core"
6
6
  import type { BaseHost } from "../hosts/base.js"
7
- import { type Axis, mainSize, crossSize, mainPos, crossPos, mainDim, crossDim, makeRect } from "@effect-tui/core"
7
+ import type { HostInstance, Rect, Size } from "../reconciler/types.js"
8
8
 
9
9
  export type FlexAxis = Axis
10
10
  export type FlexAlignment = "start" | "center" | "end"
@@ -53,6 +53,19 @@ export function measureFlex(
53
53
  return { sizes, totalSize }
54
54
  }
55
55
 
56
+ /**
57
+ * Get the greedy weight for a child.
58
+ * - undefined/false = 0 (not greedy, hugs content)
59
+ * - true = 1
60
+ * - number = that weight
61
+ */
62
+ function getGreedyWeight(child: HostInstance): number {
63
+ const greedy = (child as BaseHost).greedy
64
+ if (greedy === undefined || greedy === false) return 0
65
+ if (greedy === true) return 1
66
+ return greedy
67
+ }
68
+
56
69
  /**
57
70
  * Layout children along a flex axis using cached sizes.
58
71
  */
@@ -67,17 +80,17 @@ export function layoutFlex(
67
80
  ): void {
68
81
  // Calculate totals
69
82
  let totalNaturalMain = 0
70
- let totalFlexGrow = 0
83
+ let totalGreedyWeight = 0
71
84
  const totalSpacing = Math.max(0, (children.length - 1) * spacing)
72
85
 
73
86
  for (let i = 0; i < children.length; i++) {
74
87
  const child = children[i]
75
88
  const size = cachedSizes[i] ?? child.measure(rect.w, rect.h)
76
89
  totalNaturalMain += mainSize(axis, size)
77
- totalFlexGrow += (child as BaseHost).flexGrow ?? 0
90
+ totalGreedyWeight += getGreedyWeight(child)
78
91
  }
79
92
 
80
- // Calculate extra space to distribute
93
+ // Calculate extra space to distribute to greedy children
81
94
  const availableMain = mainDim(axis, rect)
82
95
  const extraSpace = Math.max(0, availableMain - totalNaturalMain - totalSpacing)
83
96
 
@@ -89,10 +102,10 @@ export function layoutFlex(
89
102
  for (let i = 0; i < children.length; i++) {
90
103
  const child = children[i]
91
104
  const size = cachedSizes[i] ?? { w: 0, h: 0 }
92
- const flexGrow = (child as BaseHost).flexGrow ?? 0
93
- const flexExtra = totalFlexGrow > 0 ? (extraSpace * flexGrow) / totalFlexGrow : 0
105
+ const greedyWeight = getGreedyWeight(child)
106
+ const greedyExtra = totalGreedyWeight > 0 ? (extraSpace * greedyWeight) / totalGreedyWeight : 0
94
107
 
95
- const childMainDim = mainSize(axis, size) + flexExtra
108
+ const childMainDim = mainSize(axis, size) + greedyExtra
96
109
  const childCrossDim = crossSize(axis, size)
97
110
 
98
111
  // Calculate cross position based on alignment
@@ -1,13 +1,13 @@
1
- export { type HAlign, type VAlign, alignInRect } from "./alignment.js"
2
- export { type BorderKind, type BorderChars, borderChars, type ClipRect, drawBorder } from "./border.js"
1
+ export { alignInRect, type HAlign, type VAlign } from "./alignment.js"
2
+ export { type BorderChars, type BorderKind, borderChars, type ClipRect, drawBorder } from "./border.js"
3
+ export { type FlexAlignment, type FlexAxis, type FlexMeasureResult, layoutFlex, measureFlex } from "./flex-layout.js"
3
4
  export { type Padding, type PaddingInput, resolvePadding } from "./padding.js"
4
5
  export {
5
- type StyleOptions,
6
- type StyleInput,
7
- toColorValue,
8
- styleSpecFromProps,
9
- styleIdFromProps,
10
6
  resolveBgStyle,
11
7
  resolveInheritedBgStyle,
8
+ type StyleInput,
9
+ type StyleOptions,
10
+ styleIdFromProps,
11
+ styleSpecFromProps,
12
+ toColorValue,
12
13
  } from "./styles.js"
13
- export { type FlexAxis, type FlexAlignment, type FlexMeasureResult, measureFlex, layoutFlex } from "./flex-layout.js"
@@ -2,7 +2,7 @@
2
2
  * Style utilities for building palette-compatible style objects.
3
3
  */
4
4
 
5
- import { parseColor, type Color, type ColorValue, type Palette, type StyleSpec } from "@effect-tui/core"
5
+ import { type Color, type ColorValue, type Palette, parseColor, type StyleSpec } from "@effect-tui/core"
6
6
 
7
7
  export interface StyleOptions {
8
8
  fg?: ColorValue
@@ -56,8 +56,8 @@ export function resolveBgStyle(palette: Palette, bg?: Color): { value?: ColorVal
56
56
  return { value, styleId }
57
57
  }
58
58
 
59
- import type { HostInstance } from "../reconciler/types.js"
60
59
  import { getInheritedBg } from "../hosts/base.js"
60
+ import type { HostInstance } from "../reconciler/types.js"
61
61
 
62
62
  /**
63
63
  * Resolve background style, inheriting from parent if not explicitly set.
@@ -1,9 +1,9 @@
1
1
  // Effect visualization wrapper using React
2
2
  // Uses Effect Layer for proper terminal state management across multiple calls
3
3
 
4
- import { useState, useEffect } from "react"
4
+ import { ANSI, Colors } from "@effect-tui/core"
5
5
  import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect"
6
- import { Colors, ANSI } from "@effect-tui/core"
6
+ import { useEffect, useState } from "react"
7
7
  import { createRenderer, createRoot } from "../renderer.js"
8
8
 
9
9
  const SPINNER_FRAMES = ["⠋", "⠙", "⠸", "⠴", "⠦", "⠇"] as const