@effect-tui/react 0.2.0 → 0.2.2

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 (289) 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 +3 -3
  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 +11 -13
  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/exit.d.ts +7 -0
  52. package/dist/src/exit.d.ts.map +1 -0
  53. package/dist/src/exit.js +9 -0
  54. package/dist/src/exit.js.map +1 -0
  55. package/dist/src/highlight.d.ts +1 -1
  56. package/dist/src/highlight.d.ts.map +1 -1
  57. package/dist/src/highlight.js.map +1 -1
  58. package/dist/src/hooks/index.d.ts +3 -3
  59. package/dist/src/hooks/index.d.ts.map +1 -1
  60. package/dist/src/hooks/index.js.map +1 -1
  61. package/dist/src/hooks/use-keyboard.d.ts.map +1 -1
  62. package/dist/src/hooks/use-keyboard.js.map +1 -1
  63. package/dist/src/hooks/use-mouse.d.ts +1 -1
  64. package/dist/src/hooks/use-mouse.d.ts.map +1 -1
  65. package/dist/src/hooks/use-mouse.js.map +1 -1
  66. package/dist/src/hooks/use-quit.d.ts.map +1 -1
  67. package/dist/src/hooks/use-quit.js +2 -1
  68. package/dist/src/hooks/use-quit.js.map +1 -1
  69. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  70. package/dist/src/hooks/use-scroll.js +2 -2
  71. package/dist/src/hooks/use-scroll.js.map +1 -1
  72. package/dist/src/hooks/useFrameStats.d.ts.map +1 -1
  73. package/dist/src/hooks/useFrameStats.js.map +1 -1
  74. package/dist/src/hosts/base.d.ts +2 -2
  75. package/dist/src/hosts/base.d.ts.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 +6 -3
  81. package/dist/src/hosts/canvas.d.ts.map +1 -1
  82. package/dist/src/hosts/canvas.js +9 -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 +1 -1
  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 +2 -2
  103. package/dist/src/hosts/scroll.d.ts.map +1 -1
  104. package/dist/src/hosts/scroll.js +1 -1
  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 +1 -1
  109. package/dist/src/hosts/spacer.d.ts.map +1 -1
  110. package/dist/src/hosts/text.d.ts +2 -2
  111. package/dist/src/hosts/text.d.ts.map +1 -1
  112. package/dist/src/hosts/text.js +1 -1
  113. package/dist/src/hosts/text.js.map +1 -1
  114. package/dist/src/hosts/vstack.d.ts +1 -1
  115. package/dist/src/hosts/zstack.d.ts +2 -2
  116. package/dist/src/hosts/zstack.d.ts.map +1 -1
  117. package/dist/src/hosts/zstack.js +1 -1
  118. package/dist/src/hosts/zstack.js.map +1 -1
  119. package/dist/src/index.d.ts +17 -21
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +11 -15
  122. package/dist/src/index.js.map +1 -1
  123. package/dist/src/inline/index.d.ts.map +1 -1
  124. package/dist/src/inline/index.js +1 -1
  125. package/dist/src/inline/index.js.map +1 -1
  126. package/dist/src/motion/color-motion-value.d.ts +1 -1
  127. package/dist/src/motion/color-motion-value.d.ts.map +1 -1
  128. package/dist/src/motion/color-motion-value.js.map +1 -1
  129. package/dist/src/motion/color.d.ts +1 -1
  130. package/dist/src/motion/color.d.ts.map +1 -1
  131. package/dist/src/motion/color.js +1 -1
  132. package/dist/src/motion/color.js.map +1 -1
  133. package/dist/src/motion/color.test.js +2 -2
  134. package/dist/src/motion/color.test.js.map +1 -1
  135. package/dist/src/motion/hooks.d.ts +3 -3
  136. package/dist/src/motion/hooks.d.ts.map +1 -1
  137. package/dist/src/motion/hooks.js +1 -1
  138. package/dist/src/motion/hooks.js.map +1 -1
  139. package/dist/src/motion/index.d.ts +2 -2
  140. package/dist/src/motion/index.d.ts.map +1 -1
  141. package/dist/src/motion/index.js +3 -3
  142. package/dist/src/motion/index.js.map +1 -1
  143. package/dist/src/motion/motion-value.d.ts +5 -5
  144. package/dist/src/motion/motion-value.d.ts.map +1 -1
  145. package/dist/src/motion/motion-value.js +4 -4
  146. package/dist/src/motion/motion-value.js.map +1 -1
  147. package/dist/src/motion/motion-value.test.js +1 -1
  148. package/dist/src/motion/motion-value.test.js.map +1 -1
  149. package/dist/src/reconciler/host-config.d.ts +1 -1
  150. package/dist/src/reconciler/host-config.d.ts.map +1 -1
  151. package/dist/src/reconciler/host-config.js +1 -1
  152. package/dist/src/reconciler/host-config.js.map +1 -1
  153. package/dist/src/reconciler/types.d.ts +1 -1
  154. package/dist/src/remote/Procedures.d.ts.map +1 -1
  155. package/dist/src/remote/Procedures.js.map +1 -1
  156. package/dist/src/remote/Router.d.ts +1 -1
  157. package/dist/src/remote/Router.d.ts.map +1 -1
  158. package/dist/src/remote/Router.js +1 -1
  159. package/dist/src/remote/Router.js.map +1 -1
  160. package/dist/src/remote/Server.d.ts +1 -1
  161. package/dist/src/remote/Server.d.ts.map +1 -1
  162. package/dist/src/remote/Server.js +3 -3
  163. package/dist/src/remote/Server.js.map +1 -1
  164. package/dist/src/remote/index.d.ts +2 -2
  165. package/dist/src/remote/index.d.ts.map +1 -1
  166. package/dist/src/remote/index.js +5 -5
  167. package/dist/src/remote/index.js.map +1 -1
  168. package/dist/src/renderer/core/FrameBuilder.d.ts.map +1 -1
  169. package/dist/src/renderer/core/FrameBuilder.js.map +1 -1
  170. package/dist/src/renderer/input/InputProcessor.js +1 -1
  171. package/dist/src/renderer/input/InputProcessor.js.map +1 -1
  172. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts +1 -1
  173. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts.map +1 -1
  174. package/dist/src/renderer/lifecycle/index.d.ts +1 -1
  175. package/dist/src/renderer/lifecycle/index.d.ts.map +1 -1
  176. package/dist/src/renderer/lifecycle/index.js +1 -1
  177. package/dist/src/renderer/lifecycle/index.js.map +1 -1
  178. package/dist/src/renderer/modes/FullscreenRenderer.d.ts +1 -1
  179. package/dist/src/renderer/modes/FullscreenRenderer.d.ts.map +1 -1
  180. package/dist/src/renderer/modes/InlineRenderer.d.ts +1 -1
  181. package/dist/src/renderer/modes/InlineRenderer.d.ts.map +1 -1
  182. package/dist/src/renderer/modes/InlineRenderer.js +1 -1
  183. package/dist/src/renderer/modes/InlineRenderer.js.map +1 -1
  184. package/dist/src/renderer/modes/StaticContentRenderer.d.ts.map +1 -1
  185. package/dist/src/renderer/modes/StaticContentRenderer.js.map +1 -1
  186. package/dist/src/renderer/modes/index.d.ts +1 -1
  187. package/dist/src/renderer/modes/index.d.ts.map +1 -1
  188. package/dist/src/renderer/modes/index.js.map +1 -1
  189. package/dist/src/renderer-context.js +1 -1
  190. package/dist/src/renderer-context.js.map +1 -1
  191. package/dist/src/renderer-types.d.ts +7 -1
  192. package/dist/src/renderer-types.d.ts.map +1 -1
  193. package/dist/src/renderer.d.ts +2 -2
  194. package/dist/src/renderer.d.ts.map +1 -1
  195. package/dist/src/renderer.js +41 -16
  196. package/dist/src/renderer.js.map +1 -1
  197. package/dist/src/test/index.d.ts +2 -2
  198. package/dist/src/test/index.d.ts.map +1 -1
  199. package/dist/src/test/index.js +1 -1
  200. package/dist/src/test/index.js.map +1 -1
  201. package/dist/src/test/render-tui.d.ts +2 -2
  202. package/dist/src/test/render-tui.d.ts.map +1 -1
  203. package/dist/src/test/render-tui.js +2 -2
  204. package/dist/src/test/render-tui.js.map +1 -1
  205. package/dist/src/utils/flex-layout.d.ts +15 -1
  206. package/dist/src/utils/flex-layout.d.ts.map +1 -1
  207. package/dist/src/utils/flex-layout.js +79 -24
  208. package/dist/src/utils/flex-layout.js.map +1 -1
  209. package/dist/src/utils/index.d.ts +4 -4
  210. package/dist/src/utils/index.d.ts.map +1 -1
  211. package/dist/src/utils/index.js +2 -2
  212. package/dist/src/utils/index.js.map +1 -1
  213. package/dist/src/utils/styles.d.ts.map +1 -1
  214. package/dist/src/utils/styles.js.map +1 -1
  215. package/dist/src/visualize/index.js +2 -2
  216. package/dist/src/visualize/index.js.map +1 -1
  217. package/dist/tsconfig.tsbuildinfo +1 -1
  218. package/jsx-runtime.ts +6 -6
  219. package/package.json +2 -2
  220. package/src/codeblock.tsx +1 -1
  221. package/src/components/Divider.tsx +1 -1
  222. package/src/components/Markdown.tsx +2 -2
  223. package/src/components/MultilineTextInput.tsx +9 -9
  224. package/src/components/Static.tsx +1 -1
  225. package/src/components/TextInput.tsx +9 -9
  226. package/src/components/index.ts +4 -4
  227. package/src/console/ConsoleCapture.ts +1 -1
  228. package/src/console/ConsolePopover.tsx +112 -119
  229. package/src/console/index.ts +1 -3
  230. package/src/console/useConsole.ts +2 -2
  231. package/src/debug/DebugOverlay.ts +3 -6
  232. package/src/dev/Toast.tsx +12 -10
  233. package/src/dev/index.ts +1 -1
  234. package/src/dev.tsx +11 -12
  235. package/src/exit.ts +8 -0
  236. package/src/highlight.ts +1 -1
  237. package/src/hooks/index.ts +3 -3
  238. package/src/hooks/use-keyboard.ts +1 -1
  239. package/src/hooks/use-mouse.ts +1 -1
  240. package/src/hooks/use-quit.ts +2 -1
  241. package/src/hooks/use-scroll.ts +9 -11
  242. package/src/hooks/useFrameStats.ts +1 -1
  243. package/src/hosts/base.ts +2 -2
  244. package/src/hosts/box.ts +4 -4
  245. package/src/hosts/canvas.ts +14 -3
  246. package/src/hosts/codeblock.ts +3 -3
  247. package/src/hosts/flex-container.ts +3 -3
  248. package/src/hosts/hstack.ts +1 -1
  249. package/src/hosts/index.ts +14 -14
  250. package/src/hosts/overlay-item.ts +1 -1
  251. package/src/hosts/overlay.ts +2 -2
  252. package/src/hosts/scroll.ts +3 -3
  253. package/src/hosts/single-child.ts +1 -1
  254. package/src/hosts/spacer.ts +1 -1
  255. package/src/hosts/text.ts +3 -3
  256. package/src/hosts/vstack.ts +1 -1
  257. package/src/hosts/zstack.ts +2 -2
  258. package/src/index.ts +60 -60
  259. package/src/inline/index.tsx +1 -1
  260. package/src/motion/color-motion-value.ts +1 -1
  261. package/src/motion/color.test.ts +2 -2
  262. package/src/motion/color.ts +2 -2
  263. package/src/motion/hooks.ts +3 -3
  264. package/src/motion/index.ts +8 -8
  265. package/src/motion/motion-value.test.ts +1 -1
  266. package/src/motion/motion-value.ts +11 -11
  267. package/src/reconciler/host-config.ts +3 -3
  268. package/src/reconciler/types.ts +1 -1
  269. package/src/remote/Procedures.ts +1 -7
  270. package/src/remote/Router.ts +3 -7
  271. package/src/remote/Server.ts +7 -12
  272. package/src/remote/index.ts +6 -10
  273. package/src/renderer/core/FrameBuilder.ts +1 -1
  274. package/src/renderer/input/InputProcessor.ts +1 -1
  275. package/src/renderer/lifecycle/TerminalSetup.ts +1 -1
  276. package/src/renderer/lifecycle/index.ts +1 -1
  277. package/src/renderer/modes/FullscreenRenderer.ts +1 -1
  278. package/src/renderer/modes/InlineRenderer.ts +2 -2
  279. package/src/renderer/modes/StaticContentRenderer.ts +1 -1
  280. package/src/renderer/modes/index.ts +1 -1
  281. package/src/renderer-context.ts +1 -1
  282. package/src/renderer-types.ts +7 -1
  283. package/src/renderer.ts +53 -28
  284. package/src/test/index.ts +4 -4
  285. package/src/test/render-tui.ts +3 -3
  286. package/src/utils/flex-layout.ts +83 -24
  287. package/src/utils/index.ts +8 -8
  288. package/src/utils/styles.ts +2 -2
  289. package/src/visualize/index.tsx +2 -2
@@ -1,7 +1,7 @@
1
1
  // RPC handlers for remote session control
2
2
  import type { Rpc } from "@effect/rpc"
3
- import { Effect, Layer, Context } from "effect"
4
3
  import type { KeyMsg } from "@effect-tui/core"
4
+ import { Context, Effect, type Layer } from "effect"
5
5
  import { TuiRpcs } from "./Procedures.js"
6
6
 
7
7
  // Service interface for the TUI session
@@ -19,10 +19,7 @@ export interface TuiSessionImpl {
19
19
  }
20
20
  }
21
21
 
22
- export class TuiSession extends Context.Tag("TuiSession")<
23
- TuiSession,
24
- TuiSessionImpl
25
- >() {}
22
+ export class TuiSession extends Context.Tag("TuiSession")<TuiSession, TuiSessionImpl>() {}
26
23
 
27
24
  // Create the RPC handlers layer
28
25
  export const HandlersLive: Layer.Layer<
@@ -55,8 +52,7 @@ export const HandlersLive: Layer.Layer<
55
52
 
56
53
  Paste: ({ text }) => Effect.sync(() => session.dispatchPaste(text)),
57
54
 
58
- Resize: ({ width, height }) =>
59
- Effect.sync(() => session.dispatchResize(width, height)),
55
+ Resize: ({ width, height }) => Effect.sync(() => session.dispatchResize(width, height)),
60
56
 
61
57
  Info: () => Effect.sync(() => session.getInfo()),
62
58
  }
@@ -1,9 +1,10 @@
1
1
  // Remote server that exposes TUI session via Unix socket
2
- import { Effect, Layer } from "effect"
3
- import { RpcSerialization, RpcServer } from "@effect/rpc"
4
- import { BunSocketServer } from "@effect/platform-bun"
2
+
5
3
  import * as fs from "node:fs"
6
4
  import * as path from "node:path"
5
+ import { BunSocketServer } from "@effect/platform-bun"
6
+ import { RpcSerialization, RpcServer } from "@effect/rpc"
7
+ import { Effect, Layer } from "effect"
7
8
  import { TuiRpcs } from "./Procedures.js"
8
9
  import { HandlersLive, TuiSession, type TuiSessionImpl } from "./Router.js"
9
10
 
@@ -18,8 +19,7 @@ const ensureSocketDir = Effect.sync(() => {
18
19
  })
19
20
 
20
21
  // Get socket path for current process
21
- export const getSocketPath = (pid?: number) =>
22
- path.join(SOCKET_DIR, `${pid ?? process.pid}.sock`)
22
+ export const getSocketPath = (pid?: number) => path.join(SOCKET_DIR, `${pid ?? process.pid}.sock`)
23
23
 
24
24
  // Clean up socket file
25
25
  const cleanupSocket = (socketPath: string) =>
@@ -34,10 +34,7 @@ const cleanupSocket = (socketPath: string) =>
34
34
  })
35
35
 
36
36
  // Create the server layer
37
- export const makeServerLayer = (
38
- sessionImpl: TuiSessionImpl,
39
- socketPath?: string,
40
- ) => {
37
+ export const makeServerLayer = (sessionImpl: TuiSessionImpl, socketPath?: string) => {
41
38
  const actualPath = socketPath ?? getSocketPath()
42
39
 
43
40
  // Setup layer - creates directory, cleans stale socket
@@ -52,9 +49,7 @@ export const makeServerLayer = (
52
49
  )
53
50
 
54
51
  // RPC handlers layer (needs TuiSession)
55
- const HandlersLayer = HandlersLive.pipe(
56
- Layer.provide(Layer.succeed(TuiSession, sessionImpl)),
57
- )
52
+ const HandlersLayer = HandlersLive.pipe(Layer.provide(Layer.succeed(TuiSession, sessionImpl)))
58
53
 
59
54
  // Full RPC server stack:
60
55
  // 1. RpcServer.layer creates the RPC server, needs handlers
@@ -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
@@ -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"
@@ -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 {
@@ -75,6 +75,12 @@ export interface RendererOptions {
75
75
  mode?: "fullscreen" | "inline"
76
76
  /** Exit the process on Ctrl+C unless preventDefault was called. Defaults to true. */
77
77
  exitOnCtrlC?: boolean
78
+ /** Handle SIGINT/SIGTERM and restore terminal. Defaults to true. */
79
+ handleSignals?: boolean
80
+ /** Exit the process after handling SIGINT/SIGTERM. Defaults to true. */
81
+ exitOnSignal?: boolean
82
+ /** Override exit codes for signals. Defaults: SIGINT=130, SIGTERM=143. */
83
+ signalExitCodes?: Partial<Record<"SIGINT" | "SIGTERM", number>>
78
84
  /** Enable diffed rendering (per-line). Defaults to true in runtime, false in manualMode (tests). */
79
85
  diff?: boolean
80
86
  /** Skip automatic render loop. Call flush() manually to render frames. */
package/src/renderer.ts CHANGED
@@ -1,30 +1,32 @@
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 { requestExit } from "./exit.js"
6
+ import * as Prof from "./profiler.js"
7
+ import { flushSync, reconciler } from "./reconciler/host-config.js"
8
+ import type { HostContext } from "./reconciler/types.js"
9
+ // Extracted modules
10
+ import { FrameBuilder, RendererState } from "./renderer/core/index.js"
11
+ import { InputProcessor } from "./renderer/input/index.js"
12
+ import { EventBus, TerminalSetup } from "./renderer/lifecycle/index.js"
13
+ import { FullscreenRenderer, InlineRenderer, StaticContentRenderer } from "./renderer/modes/index.js"
14
+ import { RendererContext } from "./renderer-context.js"
8
15
  import type {
9
- TuiWriteStream,
16
+ Container,
17
+ FrameStats,
18
+ RendererOptions,
10
19
  TuiReadStream,
11
20
  TuiRenderer,
12
21
  TuiRendererInternal,
13
- Container,
14
- RendererOptions,
15
- FrameStats,
22
+ TuiWriteStream,
16
23
  } 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
24
 
25
- // Re-export types and context for backwards compatibility
26
- export type { TuiWriteStream, TuiReadStream, TuiRenderer, RendererOptions, FrameStats } from "./renderer-types.js"
27
25
  export { RendererContext, useRenderer, useTerminalSize } from "./renderer-context.js"
26
+ // Re-export types and context for backwards compatibility
27
+ export type { FrameStats, RendererOptions, TuiReadStream, TuiRenderer, TuiWriteStream } from "./renderer-types.js"
28
+
29
+ type HandledSignal = "SIGINT" | "SIGTERM"
28
30
 
29
31
  export function createRenderer(options?: RendererOptions): TuiRenderer {
30
32
  const fps = options?.fps ?? DEFAULT_FPS
@@ -32,6 +34,13 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
32
34
  const stdin: TuiReadStream = options?.stdin ?? process.stdin
33
35
  const mode = options?.mode ?? "fullscreen"
34
36
  const exitOnCtrlC = options?.exitOnCtrlC ?? true
37
+ const handleSignals = options?.handleSignals ?? true
38
+ const exitOnSignal = options?.exitOnSignal ?? true
39
+ const signalExitCodes: Record<HandledSignal, number> = {
40
+ SIGINT: 130,
41
+ SIGTERM: 143,
42
+ ...options?.signalExitCodes,
43
+ }
35
44
  const manualMode = options?.manualMode ?? false
36
45
  const enableDiff = options?.diff ?? !manualMode
37
46
  const skipTerminalSetup = options?.skipTerminalSetup ?? false
@@ -74,7 +83,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
74
83
  onQuit: () => {
75
84
  // Clean up terminal state before exiting
76
85
  renderer.stop()
77
- process.exit(0)
86
+ requestExit(0)
78
87
  },
79
88
  })
80
89
 
@@ -167,7 +176,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
167
176
  })
168
177
 
169
178
  // Combine static + dynamic output for atomic write
170
- let output = staticOutput + modeOutput + state.palette.sgr(0)
179
+ const output = staticOutput + modeOutput + state.palette.sgr(0)
171
180
  contentH = contentHeight
172
181
  const diffAnsiMs = performance.now() - diffStartMs
173
182
  Prof.endPhase("diff+ansi", t)
@@ -211,6 +220,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
211
220
  }
212
221
  }
213
222
 
223
+ let onExit: (() => void) | null = null
224
+ let onSignal: ((signal: NodeJS.Signals) => void) | null = null
225
+
214
226
  // Build renderer object
215
227
  const renderer: TuiRenderer = {
216
228
  get width() {
@@ -229,6 +241,15 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
229
241
  onFrameStats: (handler: (stats: FrameStats) => void) => events.onFrameStats(handler),
230
242
  stop() {
231
243
  state.running = false
244
+ if (onExit) {
245
+ process.off("exit", onExit)
246
+ onExit = null
247
+ }
248
+ if (onSignal) {
249
+ process.off("SIGINT", onSignal)
250
+ process.off("SIGTERM", onSignal)
251
+ onSignal = null
252
+ }
232
253
  if (state.loop) {
233
254
  clearInterval(state.loop)
234
255
  state.loop = null
@@ -315,16 +336,20 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
315
336
  }
316
337
 
317
338
  // Handle normal process exit (synchronous - runs before exit completes)
318
- const onExit = () => cleanup()
339
+ onExit = () => cleanup()
319
340
  process.on("exit", onExit)
320
341
 
321
- // Handle SIGINT (Ctrl+C from shell, not from TUI input) and SIGTERM
322
- const onSignal = () => {
323
- cleanup()
324
- process.exit(0)
342
+ if (handleSignals) {
343
+ // Handle SIGINT (Ctrl+C from shell, not from TUI input) and SIGTERM
344
+ onSignal = (signal: NodeJS.Signals) => {
345
+ cleanup()
346
+ if (!exitOnSignal) return
347
+ const code = signalExitCodes[signal as HandledSignal] ?? 0
348
+ requestExit(code)
349
+ }
350
+ process.on("SIGINT", onSignal)
351
+ process.on("SIGTERM", onSignal)
325
352
  }
326
- process.on("SIGINT", onSignal)
327
- process.on("SIGTERM", onSignal)
328
353
 
329
354
  ;(renderer as TuiRendererInternal)._container = null
330
355
  return renderer
@@ -436,7 +461,7 @@ export function render(element: ReactNode, options?: RendererOptions): RenderIns
436
461
  // The createRenderer exit handler will also run, but it's idempotent
437
462
  const quit = (code = 0) => {
438
463
  renderer.stop()
439
- process.exit(code)
464
+ requestExit(code)
440
465
  }
441
466
 
442
467
  return {
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,8 +1,8 @@
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
@@ -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"
@@ -17,6 +17,13 @@ export interface FlexMeasureResult {
17
17
  /**
18
18
  * Measure children along a flex axis.
19
19
  * Returns cached sizes and total size.
20
+ *
21
+ * SwiftUI-style measure:
22
+ * 1. First measure all non-greedy children with full available space
23
+ * 2. Then measure greedy children with remaining space
24
+ *
25
+ * This ensures non-greedy elements always get their natural size,
26
+ * even when sibling greedy elements have large content.
20
27
  */
21
28
  export function measureFlex(
22
29
  axis: FlexAxis,
@@ -25,26 +32,58 @@ export function measureFlex(
25
32
  maxMain: number,
26
33
  maxCross: number,
27
34
  ): FlexMeasureResult {
28
- const sizes: Size[] = []
29
- let totalMain = 0
35
+ const sizes: Size[] = new Array(children.length)
30
36
  let maxChildCross = 0
37
+ const totalSpacing = Math.max(0, (children.length - 1) * spacing)
31
38
 
39
+ // Pass 1: Measure non-greedy children first with full available space
40
+ let nonGreedyTotal = 0
32
41
  for (let i = 0; i < children.length; i++) {
33
42
  const child = children[i]
34
- const remainingMain = Math.max(0, maxMain - totalMain)
43
+ const greedyWeight = getGreedyWeight(child)
35
44
 
36
- // Convert main/cross back to width/height for child measure
37
- const childMaxW = axis === "vertical" ? maxCross : remainingMain
38
- const childMaxH = axis === "vertical" ? remainingMain : maxCross
45
+ if (greedyWeight === 0) {
46
+ // Non-greedy: measure with full available space
47
+ const childMaxW = axis === "vertical" ? maxCross : maxMain
48
+ const childMaxH = axis === "vertical" ? maxMain : maxCross
49
+ const size = child.measure(childMaxW, childMaxH)
50
+ sizes[i] = size
51
+ nonGreedyTotal += mainSize(axis, size)
52
+ maxChildCross = Math.max(maxChildCross, crossSize(axis, size))
53
+ }
54
+ }
39
55
 
40
- const size = child.measure(childMaxW, childMaxH)
41
- sizes.push(size)
56
+ // Pass 2: Measure greedy children with remaining space
57
+ const remainingForGreedy = Math.max(0, maxMain - nonGreedyTotal - totalSpacing)
58
+ let totalGreedyWeight = 0
59
+ for (let i = 0; i < children.length; i++) {
60
+ const greedyWeight = getGreedyWeight(children[i])
61
+ if (greedyWeight > 0) totalGreedyWeight += greedyWeight
62
+ }
63
+
64
+ let greedyMeasuredTotal = 0
65
+ for (let i = 0; i < children.length; i++) {
66
+ const child = children[i]
67
+ const greedyWeight = getGreedyWeight(child)
42
68
 
43
- totalMain += mainSize(axis, size)
44
- if (i < children.length - 1) totalMain += spacing
45
- maxChildCross = Math.max(maxChildCross, crossSize(axis, size))
69
+ if (greedyWeight > 0) {
70
+ // Greedy: measure with proportional share of remaining space
71
+ const greedyMain = totalGreedyWeight > 0
72
+ ? (remainingForGreedy * greedyWeight) / totalGreedyWeight
73
+ : remainingForGreedy
74
+ const childMaxW = axis === "vertical" ? maxCross : greedyMain
75
+ const childMaxH = axis === "vertical" ? greedyMain : maxCross
76
+ const size = child.measure(childMaxW, childMaxH)
77
+ sizes[i] = size
78
+ greedyMeasuredTotal += mainSize(axis, size)
79
+ maxChildCross = Math.max(maxChildCross, crossSize(axis, size))
80
+ }
46
81
  }
47
82
 
83
+ // Calculate total main dimension
84
+ // Use actual measured sizes, not the constraint
85
+ let totalMain = nonGreedyTotal + greedyMeasuredTotal + totalSpacing
86
+
48
87
  // Build total size from main/cross dimensions
49
88
  const totalW = axis === "vertical" ? maxChildCross : totalMain
50
89
  const totalH = axis === "vertical" ? totalMain : maxChildCross
@@ -68,6 +107,13 @@ function getGreedyWeight(child: HostInstance): number {
68
107
 
69
108
  /**
70
109
  * Layout children along a flex axis using cached sizes.
110
+ *
111
+ * SwiftUI-style layout:
112
+ * 1. Non-greedy children get their natural (measured) size
113
+ * 2. Greedy children share the REMAINING space proportionally
114
+ *
115
+ * This ensures non-greedy elements (like footers) are always visible,
116
+ * even when greedy elements (like scroll views) have overflowing content.
71
117
  */
72
118
  export function layoutFlex(
73
119
  axis: FlexAxis,
@@ -78,23 +124,29 @@ export function layoutFlex(
78
124
  alignment: FlexAlignment,
79
125
  stretchCross: boolean,
80
126
  ): void {
81
- // Calculate totals
82
- let totalNaturalMain = 0
83
- let totalGreedyWeight = 0
84
127
  const totalSpacing = Math.max(0, (children.length - 1) * spacing)
128
+ const availableMain = mainDim(axis, rect)
129
+
130
+ // Pass 1: Calculate space needed by non-greedy children
131
+ let nonGreedyTotal = 0
132
+ let totalGreedyWeight = 0
85
133
 
86
134
  for (let i = 0; i < children.length; i++) {
87
135
  const child = children[i]
88
136
  const size = cachedSizes[i] ?? child.measure(rect.w, rect.h)
89
- totalNaturalMain += mainSize(axis, size)
90
- totalGreedyWeight += getGreedyWeight(child)
137
+ const greedyWeight = getGreedyWeight(child)
138
+
139
+ if (greedyWeight === 0) {
140
+ // Non-greedy: use natural size
141
+ nonGreedyTotal += mainSize(axis, size)
142
+ }
143
+ totalGreedyWeight += greedyWeight
91
144
  }
92
145
 
93
- // Calculate extra space to distribute to greedy children
94
- const availableMain = mainDim(axis, rect)
95
- const extraSpace = Math.max(0, availableMain - totalNaturalMain - totalSpacing)
146
+ // Remaining space for greedy children (after non-greedy + spacing)
147
+ const remainingForGreedy = Math.max(0, availableMain - nonGreedyTotal - totalSpacing)
96
148
 
97
- // Layout children
149
+ // Pass 2: Layout all children
98
150
  let curMainPos = mainPos(axis, rect)
99
151
  const crossStartPos = crossPos(axis, rect)
100
152
  const crossDimVal = crossDim(axis, rect)
@@ -103,9 +155,16 @@ export function layoutFlex(
103
155
  const child = children[i]
104
156
  const size = cachedSizes[i] ?? { w: 0, h: 0 }
105
157
  const greedyWeight = getGreedyWeight(child)
106
- const greedyExtra = totalGreedyWeight > 0 ? (extraSpace * greedyWeight) / totalGreedyWeight : 0
107
158
 
108
- const childMainDim = mainSize(axis, size) + greedyExtra
159
+ let childMainDim: number
160
+ if (greedyWeight > 0 && totalGreedyWeight > 0) {
161
+ // Greedy: gets proportional share of remaining space
162
+ childMainDim = (remainingForGreedy * greedyWeight) / totalGreedyWeight
163
+ } else {
164
+ // Non-greedy: gets natural size
165
+ childMainDim = mainSize(axis, size)
166
+ }
167
+
109
168
  const childCrossDim = crossSize(axis, size)
110
169
 
111
170
  // 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