@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
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { Rect, Size } from "../reconciler/types.js"
6
6
 
7
- export type HAlign = "left" | "center" | "right" | "leading" | "trailing"
7
+ export type HAlign = "left" | "center" | "right"
8
8
  export type VAlign = "top" | "center" | "bottom"
9
9
 
10
10
  /**
@@ -22,14 +22,12 @@ export function alignInRect(
22
22
 
23
23
  switch (hAlign) {
24
24
  case "left":
25
- case "leading":
26
25
  x = rect.x
27
26
  break
28
27
  case "center":
29
28
  x = rect.x + Math.floor((rect.w - size.w) / 2)
30
29
  break
31
30
  case "right":
32
- case "trailing":
33
31
  x = rect.x + Math.max(0, rect.w - size.w)
34
32
  break
35
33
  }
@@ -0,0 +1,86 @@
1
+ export interface SelectionPoint {
2
+ line: number
3
+ col: number
4
+ }
5
+
6
+ export function formatTimestamp(date: Date): string {
7
+ const h = date.getHours().toString().padStart(2, "0")
8
+ const m = date.getMinutes().toString().padStart(2, "0")
9
+ const s = date.getSeconds().toString().padStart(2, "0")
10
+ return `${h}:${m}:${s}`
11
+ }
12
+
13
+ export function formatLocation(loc?: { file: string; line: number }): string {
14
+ if (!loc) return ""
15
+ const parts = loc.file.split("/")
16
+ const filename = parts[parts.length - 1]
17
+ return `${filename}:${loc.line}`
18
+ }
19
+
20
+ export function wrapLine(text: string, maxWidth: number): string[] {
21
+ if (text.length <= maxWidth) return [text]
22
+
23
+ const lines: string[] = []
24
+ let remaining = text
25
+
26
+ while (remaining.length > 0) {
27
+ if (remaining.length <= maxWidth) {
28
+ lines.push(remaining)
29
+ break
30
+ }
31
+
32
+ let breakAt = maxWidth
33
+ const lastSpace = remaining.lastIndexOf(" ", maxWidth)
34
+ if (lastSpace > maxWidth * 0.5) {
35
+ breakAt = lastSpace
36
+ }
37
+
38
+ lines.push(remaining.slice(0, breakAt))
39
+ remaining = remaining.slice(breakAt).trimStart()
40
+ }
41
+
42
+ return lines
43
+ }
44
+
45
+ /** Normalize selection so start is before end */
46
+ export function normalizeSelection(
47
+ anchor: SelectionPoint,
48
+ head: SelectionPoint,
49
+ ): [SelectionPoint, SelectionPoint] {
50
+ if (anchor.line < head.line || (anchor.line === head.line && anchor.col <= head.col)) {
51
+ return [anchor, head]
52
+ }
53
+ return [head, anchor]
54
+ }
55
+
56
+ /** Get selection bounds for a specific line */
57
+ export function getLineSelection(
58
+ lineIndex: number,
59
+ lineLength: number,
60
+ start: SelectionPoint,
61
+ end: SelectionPoint,
62
+ ): { startCol: number; endCol: number } | null {
63
+ if (lineIndex < start.line || lineIndex > end.line) return null
64
+
65
+ const startCol = lineIndex === start.line ? Math.min(start.col, lineLength) : 0
66
+ const endCol = lineIndex === end.line ? Math.min(end.col, lineLength) : lineLength
67
+
68
+ if (startCol >= endCol && lineIndex === start.line && lineIndex === end.line) return null
69
+
70
+ return { startCol, endCol }
71
+ }
72
+
73
+ export function getSelectionText(
74
+ lines: string[],
75
+ start: SelectionPoint,
76
+ end: SelectionPoint,
77
+ ): string {
78
+ const selected: string[] = []
79
+ for (let i = start.line; i <= end.line && i < lines.length; i++) {
80
+ const lineText = lines[i] ?? ""
81
+ const startCol = i === start.line ? Math.min(start.col, lineText.length) : 0
82
+ const endCol = i === end.line ? Math.min(end.col, lineText.length) : lineText.length
83
+ selected.push(lineText.slice(startCol, endCol))
84
+ }
85
+ return selected.join("\n")
86
+ }
@@ -20,5 +20,5 @@ export {
20
20
  styleSpecFromProps,
21
21
  toColorValue,
22
22
  } from "./styles.js"
23
- export { wrapSpans } from "./text-wrap.js"
23
+ export { wrapSpans, wrapText } from "./text-wrap.js"
24
24
  export { isWhitespace, matchNextWord, matchPrevWord, splitWords } from "./word-boundaries.js"
@@ -83,14 +83,26 @@ export function resolveInheritedBgStyle(
83
83
  /**
84
84
  * Fill a rect with background color, inheriting from parent if needed.
85
85
  */
86
+ export interface FillRectWithInheritedBgOptions {
87
+ /** Skip filling when there is no inherited background value. */
88
+ skipWhenUndefined?: boolean
89
+ }
90
+
86
91
  export function fillRectWithInheritedBg(
87
92
  buffer: CellBuffer,
88
93
  palette: Palette,
89
94
  rect: Rect,
90
95
  explicitBg: Color | undefined,
91
96
  parent: HostInstance | null,
92
- ): void {
93
- if (rect.w <= 0 || rect.h <= 0) return
94
- const { styleId } = resolveInheritedBgStyle(palette, explicitBg, parent)
95
- buffer.fillRect(rect.x, rect.y, rect.w, rect.h, " ".codePointAt(0)!, styleId)
97
+ options?: FillRectWithInheritedBgOptions,
98
+ ): { value?: ColorValue; styleId: number } {
99
+ if (rect.w <= 0 || rect.h <= 0) {
100
+ return { value: undefined, styleId: 0 }
101
+ }
102
+ const resolved = resolveInheritedBgStyle(palette, explicitBg, parent)
103
+ if (options?.skipWhenUndefined && resolved.value === undefined) {
104
+ return resolved
105
+ }
106
+ buffer.fillRect(rect.x, rect.y, rect.w, rect.h, " ".codePointAt(0)!, resolved.styleId)
107
+ return resolved
96
108
  }
@@ -3,59 +3,99 @@ import { isWhitespace, splitWords } from "./word-boundaries.js"
3
3
 
4
4
  type SpanLike = { text: string }
5
5
 
6
- /**
7
- * Wrap spans into lines, breaking at word boundaries.
8
- * Preserves span styling by cloning span objects with updated text.
9
- */
10
- export function wrapSpans<T extends SpanLike>(spans: T[], maxWidth: number): T[][] {
11
- const lines: T[][] = [[]]
6
+ type TokenSpec<T> = {
7
+ text: string
8
+ width: number
9
+ isWhitespace: boolean
10
+ make: (text: string) => T
11
+ }
12
+
13
+ type LineToken<T> = {
14
+ value: T
15
+ width: number
16
+ isWhitespace: boolean
17
+ }
18
+
19
+ type WrapOptions = {
20
+ trimTrailingWhitespace?: boolean
21
+ }
22
+
23
+ function wrapTokens<T>(tokens: TokenSpec<T>[], maxWidth: number, options?: WrapOptions): T[][] {
24
+ const lines: LineToken<T>[][] = [[]]
12
25
  let lineWidth = 0
13
26
 
14
- for (const span of spans) {
15
- // Split span text into words (keeping whitespace as separate tokens)
16
- const tokens = splitWords(span.text)
27
+ const trimLine = () => {
28
+ if (!options?.trimTrailingWhitespace) return
29
+ const line = lines[lines.length - 1]
30
+ while (line.length > 0 && line[line.length - 1].isWhitespace) {
31
+ const removed = line.pop()
32
+ if (removed) {
33
+ lineWidth -= removed.width
34
+ }
35
+ }
36
+ }
17
37
 
18
- for (const token of tokens) {
19
- if (!token) continue
20
- const tokenWidth = displayWidth(token)
21
- const isWs = isWhitespace(token)
22
-
23
- if (lineWidth + tokenWidth <= maxWidth) {
24
- // Token fits on current line
25
- lines[lines.length - 1].push({ ...span, text: token })
26
- lineWidth += tokenWidth
27
- } else if (isWs) {
28
- // Skip whitespace at line break
29
- continue
30
- } else if (tokenWidth <= maxWidth) {
31
- // Start new line with this token
32
- lines.push([{ ...span, text: token }])
33
- lineWidth = tokenWidth
34
- } else {
35
- // Token is longer than maxWidth - break by character
36
- let charLine = ""
37
- let charLineWidth = 0
38
- for (const ch of token) {
39
- const chWidth = displayWidth(ch)
40
- if (lineWidth + charLineWidth + chWidth > maxWidth && (charLine || lineWidth > 0)) {
41
- if (charLine) {
42
- lines[lines.length - 1].push({ ...span, text: charLine })
43
- }
44
- lines.push([])
45
- lineWidth = 0
46
- charLine = ch
47
- charLineWidth = chWidth
48
- } else {
49
- charLine += ch
50
- charLineWidth += chWidth
51
- }
52
- }
53
- if (charLine) {
54
- lines[lines.length - 1].push({ ...span, text: charLine })
55
- lineWidth += charLineWidth
38
+ const startNewLine = () => {
39
+ lines.push([])
40
+ lineWidth = 0
41
+ }
42
+
43
+ const addToken = (token: TokenSpec<T>, text: string, width: number, isWhitespace: boolean) => {
44
+ lines[lines.length - 1].push({ value: token.make(text), width, isWhitespace })
45
+ lineWidth += width
46
+ }
47
+
48
+ for (const token of tokens) {
49
+ if (!token.text) continue
50
+ const tokenWidth = token.width
51
+ const isWs = token.isWhitespace
52
+
53
+ if (lineWidth + tokenWidth <= maxWidth) {
54
+ addToken(token, token.text, tokenWidth, isWs)
55
+ continue
56
+ }
57
+
58
+ if (isWs) {
59
+ // Skip whitespace at line break
60
+ continue
61
+ }
62
+
63
+ if (tokenWidth <= maxWidth) {
64
+ trimLine()
65
+ if (lines[lines.length - 1].length > 0) {
66
+ startNewLine()
67
+ }
68
+ addToken(token, token.text, tokenWidth, isWs)
69
+ continue
70
+ }
71
+
72
+ // Token is longer than maxWidth - break by character
73
+ trimLine()
74
+ if (lines[lines.length - 1].length > 0) {
75
+ startNewLine()
76
+ }
77
+
78
+ let segment = ""
79
+ let segmentWidth = 0
80
+
81
+ for (const ch of token.text) {
82
+ const chWidth = displayWidth(ch)
83
+ if (lineWidth + segmentWidth + chWidth > maxWidth && (segment || lineWidth > 0)) {
84
+ if (segment) {
85
+ addToken(token, segment, segmentWidth, false)
56
86
  }
87
+ startNewLine()
88
+ segment = ch
89
+ segmentWidth = chWidth
90
+ } else {
91
+ segment += ch
92
+ segmentWidth += chWidth
57
93
  }
58
94
  }
95
+
96
+ if (segment) {
97
+ addToken(token, segment, segmentWidth, false)
98
+ }
59
99
  }
60
100
 
61
101
  // Remove empty lines at the end
@@ -63,5 +103,56 @@ export function wrapSpans<T extends SpanLike>(spans: T[], maxWidth: number): T[]
63
103
  lines.pop()
64
104
  }
65
105
 
66
- return lines.length > 0 ? lines : [[]]
106
+ return lines.length > 0 ? lines.map((line) => line.map((token) => token.value)) : [[]]
107
+ }
108
+
109
+ /**
110
+ * Wrap spans into lines, breaking at word boundaries.
111
+ * Preserves span styling by cloning span objects with updated text.
112
+ */
113
+ export function wrapSpans<T extends SpanLike>(spans: T[], maxWidth: number): T[][] {
114
+ const tokens: Array<TokenSpec<T>> = []
115
+ for (const span of spans) {
116
+ for (const token of splitWords(span.text)) {
117
+ if (!token) continue
118
+ tokens.push({
119
+ text: token,
120
+ width: displayWidth(token),
121
+ isWhitespace: isWhitespace(token),
122
+ make: (text) => ({ ...span, text }),
123
+ })
124
+ }
125
+ }
126
+
127
+ return wrapTokens(tokens, maxWidth)
128
+ }
129
+
130
+ /**
131
+ * Wrap plain text into lines, breaking at word boundaries.
132
+ * Trims trailing whitespace when a line breaks.
133
+ */
134
+ export function wrapText(text: string, maxWidth: number): string[] {
135
+ const lines: string[] = []
136
+ for (const rawLine of text.split("\n")) {
137
+ if (rawLine === "") {
138
+ lines.push("")
139
+ continue
140
+ }
141
+ const tokens: Array<TokenSpec<string>> = []
142
+ for (const token of splitWords(rawLine)) {
143
+ if (!token) continue
144
+ tokens.push({
145
+ text: token,
146
+ width: displayWidth(token),
147
+ isWhitespace: isWhitespace(token),
148
+ make: (segment) => segment,
149
+ })
150
+ }
151
+ const wrapped = wrapTokens(tokens, maxWidth, { trimTrailingWhitespace: true })
152
+ for (const line of wrapped) {
153
+ lines.push(line.join(""))
154
+ }
155
+ }
156
+
157
+ return lines.length > 0 ? lines : [""]
67
158
  }
@@ -273,7 +273,7 @@ export const visualize = <A, E, R>(
273
273
  : { status: "failure", elapsedMs: Date.now() - startTime, errorSummary: summarizeCause(exit.cause) },
274
274
  )
275
275
  renderer.requestRender()
276
- renderer.flush() // Ensure final state is rendered
276
+ renderer.renderNow() // Ensure final state is rendered
277
277
 
278
278
  // Wait for completion animation
279
279
  yield* Effect.sleep(COMPLETION_DELAY_MS)
@@ -1,65 +0,0 @@
1
- import type { TuiWriteStream } from "../../renderer-types.js"
2
-
3
- export interface ResizeState {
4
- width: number
5
- height: number
6
- lastWidth: number
7
- previousHeight: number // For inline mode
8
- inlineClearHeight: number
9
- }
10
-
11
- export interface ResizeResult {
12
- newWidth: number
13
- newHeight: number
14
- needsFullClear: boolean // Fullscreen mode
15
- inlineClearHeight: number // Inline mode
16
- }
17
-
18
- /**
19
- * Handles terminal resize events and calculates necessary clear/redraw actions.
20
- */
21
- export class ResizeManager {
22
- constructor(
23
- private stdout: TuiWriteStream,
24
- private mode: "fullscreen" | "inline",
25
- ) {}
26
-
27
- /**
28
- * Process a resize event and determine what actions are needed.
29
- */
30
- handleResize(currentState: ResizeState): ResizeResult {
31
- const newWidth = this.stdout.columns || 80
32
- const newHeight = this.stdout.rows || 24
33
-
34
- let needsFullClear = false
35
- let inlineClearHeight = currentState.inlineClearHeight
36
-
37
- if (this.mode === "fullscreen") {
38
- // Fullscreen: defer clear to renderFrame so it's written atomically with content
39
- needsFullClear = true
40
- } else if (newWidth !== currentState.lastWidth || newHeight !== currentState.height) {
41
- // Inline: on dimension change, request clear on next render
42
- const clearHeight = Math.max(currentState.previousHeight, currentState.height, newHeight)
43
- if (clearHeight > inlineClearHeight) {
44
- inlineClearHeight = clearHeight
45
- }
46
- }
47
-
48
- return {
49
- newWidth,
50
- newHeight,
51
- needsFullClear,
52
- inlineClearHeight,
53
- }
54
- }
55
-
56
- /**
57
- * Get current terminal dimensions.
58
- */
59
- getDimensions(): { width: number; height: number } {
60
- return {
61
- width: this.stdout.columns || 80,
62
- height: this.stdout.rows || 24,
63
- }
64
- }
65
- }
@@ -1,4 +0,0 @@
1
- export { EventBus } from "./EventBus.js"
2
- export { getRenderCache, type RenderCache } from "./RenderCache.js"
3
- export { ResizeManager, type ResizeResult, type ResizeState } from "./ResizeManager.js"
4
- export { TerminalSetup, type TerminalSetupConfig } from "./TerminalSetup.js"