@fairyhunter13/opentui-core 0.1.114 → 0.1.116

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 (591) hide show
  1. package/3d/SpriteResourceManager.d.ts +74 -0
  2. package/3d/SpriteUtils.d.ts +13 -0
  3. package/3d/TextureUtils.d.ts +24 -0
  4. package/3d/ThreeRenderable.d.ts +40 -0
  5. package/3d/WGPURenderer.d.ts +61 -0
  6. package/3d/animation/ExplodingSpriteEffect.d.ts +71 -0
  7. package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +76 -0
  8. package/3d/animation/SpriteAnimator.d.ts +124 -0
  9. package/3d/animation/SpriteParticleGenerator.d.ts +62 -0
  10. package/3d/canvas.d.ts +44 -0
  11. package/3d/index.d.ts +12 -0
  12. package/3d/physics/PlanckPhysicsAdapter.d.ts +19 -0
  13. package/3d/physics/RapierPhysicsAdapter.d.ts +19 -0
  14. package/3d/physics/physics-interface.d.ts +27 -0
  15. package/3d.d.ts +2 -0
  16. package/3d.js +34041 -0
  17. package/3d.js.map +155 -0
  18. package/LICENSE +21 -0
  19. package/NativeSpanFeed.d.ts +41 -0
  20. package/Renderable.d.ts +334 -0
  21. package/animation/Timeline.d.ts +126 -0
  22. package/ansi.d.ts +13 -0
  23. package/buffer.d.ts +111 -0
  24. package/console.d.ts +144 -0
  25. package/edit-buffer.d.ts +98 -0
  26. package/editor-view.d.ts +73 -0
  27. package/index-dcj62y8t.js +20614 -0
  28. package/index-dcj62y8t.js.map +67 -0
  29. package/index-jyrhjc34.js +411 -0
  30. package/index-jyrhjc34.js.map +10 -0
  31. package/index-wc7ae60z.js +12299 -0
  32. package/index-wc7ae60z.js.map +42 -0
  33. package/index.d.ts +23 -0
  34. package/index.js +478 -0
  35. package/index.js.map +9 -0
  36. package/lib/KeyHandler.d.ts +61 -0
  37. package/lib/RGBA.d.ts +25 -0
  38. package/lib/ascii.font.d.ts +508 -0
  39. package/lib/border.d.ts +51 -0
  40. package/lib/bunfs.d.ts +7 -0
  41. package/lib/clipboard.d.ts +17 -0
  42. package/lib/clock.d.ts +15 -0
  43. package/lib/data-paths.d.ts +26 -0
  44. package/lib/debounce.d.ts +42 -0
  45. package/lib/detect-links.d.ts +6 -0
  46. package/lib/env.d.ts +42 -0
  47. package/lib/extmarks-history.d.ts +17 -0
  48. package/lib/extmarks.d.ts +89 -0
  49. package/lib/hast-styled-text.d.ts +17 -0
  50. package/lib/index.d.ts +21 -0
  51. package/lib/keymapping.d.ts +25 -0
  52. package/lib/objects-in-viewport.d.ts +24 -0
  53. package/lib/output.capture.d.ts +24 -0
  54. package/lib/parse.keypress-kitty.d.ts +2 -0
  55. package/lib/parse.keypress.d.ts +26 -0
  56. package/lib/parse.mouse.d.ts +30 -0
  57. package/lib/paste.d.ts +7 -0
  58. package/lib/queue.d.ts +15 -0
  59. package/lib/renderable.validations.d.ts +12 -0
  60. package/lib/scroll-acceleration.d.ts +43 -0
  61. package/lib/selection.d.ts +63 -0
  62. package/lib/singleton.d.ts +7 -0
  63. package/lib/stdin-parser.d.ts +87 -0
  64. package/lib/styled-text.d.ts +63 -0
  65. package/lib/terminal-capability-detection.d.ts +30 -0
  66. package/lib/terminal-palette.d.ts +50 -0
  67. package/lib/tree-sitter/assets/update.d.ts +11 -0
  68. package/lib/tree-sitter/client.d.ts +47 -0
  69. package/lib/tree-sitter/default-parsers.d.ts +2 -0
  70. package/lib/tree-sitter/download-utils.d.ts +21 -0
  71. package/lib/tree-sitter/index.d.ts +8 -0
  72. package/lib/tree-sitter/parser.worker.d.ts +1 -0
  73. package/lib/tree-sitter/parsers-config.d.ts +53 -0
  74. package/lib/tree-sitter/resolve-ft.d.ts +5 -0
  75. package/lib/tree-sitter/types.d.ts +82 -0
  76. package/lib/tree-sitter-styled-text.d.ts +14 -0
  77. package/lib/validate-dir-name.d.ts +1 -0
  78. package/lib/yoga.options.d.ts +32 -0
  79. package/package.json +53 -62
  80. package/parser.worker.js +899 -0
  81. package/parser.worker.js.map +12 -0
  82. package/plugins/core-slot.d.ts +72 -0
  83. package/plugins/registry.d.ts +42 -0
  84. package/plugins/types.d.ts +34 -0
  85. package/post/effects.d.ts +147 -0
  86. package/post/filters.d.ts +65 -0
  87. package/post/matrices.d.ts +20 -0
  88. package/renderables/ASCIIFont.d.ts +52 -0
  89. package/renderables/Box.d.ts +81 -0
  90. package/renderables/Code.d.ts +78 -0
  91. package/renderables/Diff.d.ts +142 -0
  92. package/renderables/EditBufferRenderable.d.ts +237 -0
  93. package/renderables/FrameBuffer.d.ts +16 -0
  94. package/renderables/Input.d.ts +67 -0
  95. package/renderables/LineNumberRenderable.d.ts +78 -0
  96. package/renderables/Markdown.d.ts +185 -0
  97. package/renderables/ScrollBar.d.ts +77 -0
  98. package/renderables/ScrollBox.d.ts +124 -0
  99. package/renderables/Select.d.ts +115 -0
  100. package/renderables/Slider.d.ts +47 -0
  101. package/renderables/TabSelect.d.ts +96 -0
  102. package/renderables/Text.d.ts +36 -0
  103. package/renderables/TextBufferRenderable.d.ts +105 -0
  104. package/renderables/TextNode.d.ts +91 -0
  105. package/renderables/TextTable.d.ts +140 -0
  106. package/renderables/Textarea.d.ts +63 -0
  107. package/renderables/TimeToFirstDraw.d.ts +24 -0
  108. package/renderables/__tests__/renderable-test-utils.d.ts +12 -0
  109. package/renderables/composition/VRenderable.d.ts +16 -0
  110. package/renderables/composition/constructs.d.ts +35 -0
  111. package/renderables/composition/vnode.d.ts +46 -0
  112. package/renderables/index.d.ts +23 -0
  113. package/renderables/markdown-parser.d.ts +10 -0
  114. package/renderer.d.ts +419 -0
  115. package/runtime-plugin-support.d.ts +3 -0
  116. package/runtime-plugin-support.js +29 -0
  117. package/runtime-plugin-support.js.map +10 -0
  118. package/runtime-plugin.d.ts +16 -0
  119. package/runtime-plugin.js +16 -0
  120. package/runtime-plugin.js.map +9 -0
  121. package/syntax-style.d.ts +54 -0
  122. package/testing/manual-clock.d.ts +17 -0
  123. package/testing/mock-keys.d.ts +81 -0
  124. package/testing/mock-mouse.d.ts +38 -0
  125. package/testing/mock-tree-sitter-client.d.ts +23 -0
  126. package/testing/spy.d.ts +7 -0
  127. package/testing/test-recorder.d.ts +61 -0
  128. package/testing/test-renderer.d.ts +23 -0
  129. package/testing.d.ts +6 -0
  130. package/testing.js +697 -0
  131. package/testing.js.map +15 -0
  132. package/text-buffer-view.d.ts +42 -0
  133. package/text-buffer.d.ts +67 -0
  134. package/types.d.ts +139 -0
  135. package/utils.d.ts +14 -0
  136. package/zig-structs.d.ts +155 -0
  137. package/zig.d.ts +353 -0
  138. package/dev/keypress-debug-renderer.ts +0 -148
  139. package/dev/keypress-debug.ts +0 -43
  140. package/dev/print-env-vars.ts +0 -32
  141. package/dev/test-tmux-graphics-334.sh +0 -68
  142. package/dev/thai-debug-test.ts +0 -68
  143. package/docs/development.md +0 -144
  144. package/scripts/build.ts +0 -400
  145. package/scripts/publish.ts +0 -60
  146. package/src/3d/SpriteResourceManager.ts +0 -286
  147. package/src/3d/SpriteUtils.ts +0 -70
  148. package/src/3d/TextureUtils.ts +0 -196
  149. package/src/3d/ThreeRenderable.ts +0 -197
  150. package/src/3d/WGPURenderer.ts +0 -294
  151. package/src/3d/animation/ExplodingSpriteEffect.ts +0 -513
  152. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +0 -429
  153. package/src/3d/animation/SpriteAnimator.ts +0 -633
  154. package/src/3d/animation/SpriteParticleGenerator.ts +0 -435
  155. package/src/3d/canvas.ts +0 -464
  156. package/src/3d/index.ts +0 -12
  157. package/src/3d/physics/PlanckPhysicsAdapter.ts +0 -72
  158. package/src/3d/physics/RapierPhysicsAdapter.ts +0 -66
  159. package/src/3d/physics/physics-interface.ts +0 -31
  160. package/src/3d/shaders/supersampling.wgsl +0 -201
  161. package/src/3d.ts +0 -3
  162. package/src/NativeSpanFeed.ts +0 -300
  163. package/src/Renderable.ts +0 -1704
  164. package/src/__snapshots__/buffer.test.ts.snap +0 -28
  165. package/src/animation/Timeline.test.ts +0 -2709
  166. package/src/animation/Timeline.ts +0 -598
  167. package/src/ansi.ts +0 -18
  168. package/src/benchmark/attenuation-benchmark.ts +0 -81
  169. package/src/benchmark/colormatrix-benchmark.ts +0 -128
  170. package/src/benchmark/gain-benchmark.ts +0 -80
  171. package/src/benchmark/latest-all-bench-run.json +0 -707
  172. package/src/benchmark/latest-async-bench-run.json +0 -336
  173. package/src/benchmark/latest-default-bench-run.json +0 -657
  174. package/src/benchmark/latest-large-bench-run.json +0 -707
  175. package/src/benchmark/latest-quick-bench-run.json +0 -207
  176. package/src/benchmark/markdown-benchmark.ts +0 -1796
  177. package/src/benchmark/native-span-feed-async-benchmark.ts +0 -355
  178. package/src/benchmark/native-span-feed-benchmark.md +0 -56
  179. package/src/benchmark/native-span-feed-benchmark.ts +0 -596
  180. package/src/benchmark/native-span-feed-compare.ts +0 -280
  181. package/src/benchmark/renderer-benchmark.ts +0 -754
  182. package/src/benchmark/text-table-benchmark.ts +0 -948
  183. package/src/buffer.test.ts +0 -291
  184. package/src/buffer.ts +0 -554
  185. package/src/console.test.ts +0 -612
  186. package/src/console.ts +0 -1254
  187. package/src/edit-buffer.test.ts +0 -1769
  188. package/src/edit-buffer.ts +0 -411
  189. package/src/editor-view.test.ts +0 -1032
  190. package/src/editor-view.ts +0 -284
  191. package/src/examples/ascii-font-selection-demo.ts +0 -245
  192. package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
  193. package/src/examples/assets/concrete.png +0 -0
  194. package/src/examples/assets/crate.png +0 -0
  195. package/src/examples/assets/crate_emissive.png +0 -0
  196. package/src/examples/assets/forrest_background.png +0 -0
  197. package/src/examples/assets/hast-example.json +0 -1018
  198. package/src/examples/assets/heart.png +0 -0
  199. package/src/examples/assets/main_char_heavy_attack.png +0 -0
  200. package/src/examples/assets/main_char_idle.png +0 -0
  201. package/src/examples/assets/main_char_jump_end.png +0 -0
  202. package/src/examples/assets/main_char_jump_landing.png +0 -0
  203. package/src/examples/assets/main_char_jump_start.png +0 -0
  204. package/src/examples/assets/main_char_run_loop.png +0 -0
  205. package/src/examples/assets/roughness_map.jpg +0 -0
  206. package/src/examples/build.ts +0 -115
  207. package/src/examples/code-demo.ts +0 -924
  208. package/src/examples/console-demo.ts +0 -358
  209. package/src/examples/core-plugin-slots-demo.ts +0 -759
  210. package/src/examples/diff-demo.ts +0 -701
  211. package/src/examples/draggable-three-demo.ts +0 -259
  212. package/src/examples/editor-demo.ts +0 -322
  213. package/src/examples/extmarks-demo.ts +0 -196
  214. package/src/examples/focus-restore-demo.ts +0 -310
  215. package/src/examples/fonts.ts +0 -245
  216. package/src/examples/fractal-shader-demo.ts +0 -268
  217. package/src/examples/framebuffer-demo.ts +0 -674
  218. package/src/examples/full-unicode-demo.ts +0 -241
  219. package/src/examples/golden-star-demo.ts +0 -933
  220. package/src/examples/grayscale-buffer-demo.ts +0 -249
  221. package/src/examples/hast-syntax-highlighting-demo.ts +0 -129
  222. package/src/examples/index.ts +0 -926
  223. package/src/examples/input-demo.ts +0 -377
  224. package/src/examples/input-select-layout-demo.ts +0 -425
  225. package/src/examples/install.sh +0 -143
  226. package/src/examples/keypress-debug-demo.ts +0 -452
  227. package/src/examples/lib/HexList.ts +0 -122
  228. package/src/examples/lib/PaletteGrid.ts +0 -125
  229. package/src/examples/lib/standalone-keys.ts +0 -25
  230. package/src/examples/lib/tab-controller.ts +0 -243
  231. package/src/examples/lights-phong-demo.ts +0 -290
  232. package/src/examples/link-demo.ts +0 -220
  233. package/src/examples/live-state-demo.ts +0 -480
  234. package/src/examples/markdown-demo.ts +0 -725
  235. package/src/examples/mouse-interaction-demo.ts +0 -428
  236. package/src/examples/nested-zindex-demo.ts +0 -357
  237. package/src/examples/opacity-example.ts +0 -235
  238. package/src/examples/opentui-demo.ts +0 -1057
  239. package/src/examples/physx-planck-2d-demo.ts +0 -623
  240. package/src/examples/physx-rapier-2d-demo.ts +0 -655
  241. package/src/examples/relative-positioning-demo.ts +0 -323
  242. package/src/examples/scroll-example.ts +0 -214
  243. package/src/examples/scrollbox-mouse-test.ts +0 -112
  244. package/src/examples/scrollbox-overlay-hit-test.ts +0 -206
  245. package/src/examples/select-demo.ts +0 -237
  246. package/src/examples/shader-cube-demo.ts +0 -1015
  247. package/src/examples/simple-layout-example.ts +0 -591
  248. package/src/examples/slider-demo.ts +0 -617
  249. package/src/examples/split-mode-demo.ts +0 -453
  250. package/src/examples/sprite-animation-demo.ts +0 -443
  251. package/src/examples/sprite-particle-generator-demo.ts +0 -486
  252. package/src/examples/static-sprite-demo.ts +0 -193
  253. package/src/examples/sticky-scroll-example.ts +0 -308
  254. package/src/examples/styled-text-demo.ts +0 -282
  255. package/src/examples/tab-select-demo.ts +0 -219
  256. package/src/examples/terminal-title.ts +0 -29
  257. package/src/examples/terminal.ts +0 -305
  258. package/src/examples/text-node-demo.ts +0 -416
  259. package/src/examples/text-selection-demo.ts +0 -377
  260. package/src/examples/text-table-demo.ts +0 -503
  261. package/src/examples/text-truncation-demo.ts +0 -481
  262. package/src/examples/text-wrap.ts +0 -757
  263. package/src/examples/texture-loading-demo.ts +0 -259
  264. package/src/examples/timeline-example.ts +0 -670
  265. package/src/examples/transparency-demo.ts +0 -400
  266. package/src/examples/vnode-composition-demo.ts +0 -404
  267. package/src/examples/wide-grapheme-overlay-demo.ts +0 -280
  268. package/src/index.ts +0 -24
  269. package/src/lib/KeyHandler.integration.test.ts +0 -292
  270. package/src/lib/KeyHandler.stopPropagation.test.ts +0 -289
  271. package/src/lib/KeyHandler.test.ts +0 -662
  272. package/src/lib/KeyHandler.ts +0 -222
  273. package/src/lib/RGBA.test.ts +0 -984
  274. package/src/lib/RGBA.ts +0 -204
  275. package/src/lib/ascii.font.ts +0 -330
  276. package/src/lib/border.test.ts +0 -83
  277. package/src/lib/border.ts +0 -170
  278. package/src/lib/bunfs.test.ts +0 -27
  279. package/src/lib/bunfs.ts +0 -18
  280. package/src/lib/clipboard.test.ts +0 -41
  281. package/src/lib/clipboard.ts +0 -47
  282. package/src/lib/clock.ts +0 -35
  283. package/src/lib/data-paths.test.ts +0 -133
  284. package/src/lib/data-paths.ts +0 -109
  285. package/src/lib/debounce.ts +0 -106
  286. package/src/lib/detect-links.test.ts +0 -98
  287. package/src/lib/detect-links.ts +0 -56
  288. package/src/lib/env.test.ts +0 -228
  289. package/src/lib/env.ts +0 -209
  290. package/src/lib/extmarks-history.ts +0 -51
  291. package/src/lib/extmarks-multiwidth.test.ts +0 -322
  292. package/src/lib/extmarks.test.ts +0 -3457
  293. package/src/lib/extmarks.ts +0 -843
  294. package/src/lib/fonts/block.json +0 -405
  295. package/src/lib/fonts/grid.json +0 -265
  296. package/src/lib/fonts/huge.json +0 -741
  297. package/src/lib/fonts/pallet.json +0 -314
  298. package/src/lib/fonts/shade.json +0 -591
  299. package/src/lib/fonts/slick.json +0 -321
  300. package/src/lib/fonts/tiny.json +0 -69
  301. package/src/lib/hast-styled-text.ts +0 -59
  302. package/src/lib/index.ts +0 -21
  303. package/src/lib/keymapping.test.ts +0 -317
  304. package/src/lib/keymapping.ts +0 -115
  305. package/src/lib/objects-in-viewport.test.ts +0 -787
  306. package/src/lib/objects-in-viewport.ts +0 -153
  307. package/src/lib/output.capture.ts +0 -58
  308. package/src/lib/parse.keypress-kitty.protocol.test.ts +0 -340
  309. package/src/lib/parse.keypress-kitty.test.ts +0 -663
  310. package/src/lib/parse.keypress-kitty.ts +0 -439
  311. package/src/lib/parse.keypress.test.ts +0 -1849
  312. package/src/lib/parse.keypress.ts +0 -397
  313. package/src/lib/parse.mouse.test.ts +0 -552
  314. package/src/lib/parse.mouse.ts +0 -232
  315. package/src/lib/paste.ts +0 -16
  316. package/src/lib/queue.ts +0 -65
  317. package/src/lib/renderable.validations.test.ts +0 -87
  318. package/src/lib/renderable.validations.ts +0 -83
  319. package/src/lib/scroll-acceleration.ts +0 -98
  320. package/src/lib/selection.ts +0 -240
  321. package/src/lib/singleton.ts +0 -28
  322. package/src/lib/stdin-parser.test.ts +0 -2290
  323. package/src/lib/stdin-parser.ts +0 -1810
  324. package/src/lib/styled-text.ts +0 -178
  325. package/src/lib/terminal-capability-detection.test.ts +0 -202
  326. package/src/lib/terminal-capability-detection.ts +0 -79
  327. package/src/lib/terminal-palette.test.ts +0 -878
  328. package/src/lib/terminal-palette.ts +0 -383
  329. package/src/lib/tree-sitter/assets/README.md +0 -118
  330. package/src/lib/tree-sitter/assets/update.ts +0 -334
  331. package/src/lib/tree-sitter/assets.d.ts +0 -9
  332. package/src/lib/tree-sitter/cache.test.ts +0 -273
  333. package/src/lib/tree-sitter/client.test.ts +0 -1165
  334. package/src/lib/tree-sitter/client.ts +0 -607
  335. package/src/lib/tree-sitter/default-parsers.ts +0 -86
  336. package/src/lib/tree-sitter/download-utils.ts +0 -148
  337. package/src/lib/tree-sitter/index.ts +0 -28
  338. package/src/lib/tree-sitter/parser.worker.ts +0 -1042
  339. package/src/lib/tree-sitter/parsers-config.ts +0 -81
  340. package/src/lib/tree-sitter/resolve-ft.test.ts +0 -55
  341. package/src/lib/tree-sitter/resolve-ft.ts +0 -189
  342. package/src/lib/tree-sitter/types.ts +0 -82
  343. package/src/lib/tree-sitter-styled-text.test.ts +0 -1253
  344. package/src/lib/tree-sitter-styled-text.ts +0 -306
  345. package/src/lib/validate-dir-name.ts +0 -55
  346. package/src/lib/yoga.options.test.ts +0 -628
  347. package/src/lib/yoga.options.ts +0 -346
  348. package/src/plugins/core-slot.ts +0 -579
  349. package/src/plugins/registry.ts +0 -402
  350. package/src/plugins/types.ts +0 -46
  351. package/src/post/effects.ts +0 -930
  352. package/src/post/filters.ts +0 -489
  353. package/src/post/matrices.ts +0 -288
  354. package/src/renderables/ASCIIFont.ts +0 -219
  355. package/src/renderables/Box.test.ts +0 -205
  356. package/src/renderables/Box.ts +0 -326
  357. package/src/renderables/Code.test.ts +0 -2062
  358. package/src/renderables/Code.ts +0 -357
  359. package/src/renderables/Diff.regression.test.ts +0 -226
  360. package/src/renderables/Diff.test.ts +0 -3101
  361. package/src/renderables/Diff.ts +0 -1211
  362. package/src/renderables/EditBufferRenderable.test.ts +0 -288
  363. package/src/renderables/EditBufferRenderable.ts +0 -1166
  364. package/src/renderables/FrameBuffer.ts +0 -47
  365. package/src/renderables/Input.test.ts +0 -1228
  366. package/src/renderables/Input.ts +0 -247
  367. package/src/renderables/LineNumberRenderable.ts +0 -724
  368. package/src/renderables/Markdown.ts +0 -1393
  369. package/src/renderables/ScrollBar.ts +0 -422
  370. package/src/renderables/ScrollBox.ts +0 -883
  371. package/src/renderables/Select.test.ts +0 -1033
  372. package/src/renderables/Select.ts +0 -524
  373. package/src/renderables/Slider.test.ts +0 -456
  374. package/src/renderables/Slider.ts +0 -342
  375. package/src/renderables/TabSelect.test.ts +0 -197
  376. package/src/renderables/TabSelect.ts +0 -455
  377. package/src/renderables/Text.selection-buffer.test.ts +0 -123
  378. package/src/renderables/Text.test.ts +0 -2660
  379. package/src/renderables/Text.ts +0 -147
  380. package/src/renderables/TextBufferRenderable.ts +0 -518
  381. package/src/renderables/TextNode.test.ts +0 -1058
  382. package/src/renderables/TextNode.ts +0 -325
  383. package/src/renderables/TextTable.test.ts +0 -1421
  384. package/src/renderables/TextTable.ts +0 -1344
  385. package/src/renderables/Textarea.ts +0 -430
  386. package/src/renderables/TimeToFirstDraw.ts +0 -89
  387. package/src/renderables/__snapshots__/Code.test.ts.snap +0 -13
  388. package/src/renderables/__snapshots__/Diff.test.ts.snap +0 -785
  389. package/src/renderables/__snapshots__/Text.test.ts.snap +0 -421
  390. package/src/renderables/__snapshots__/TextTable.test.ts.snap +0 -215
  391. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +0 -144
  392. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +0 -816
  393. package/src/renderables/__tests__/LineNumberRenderable.test.ts +0 -1865
  394. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +0 -85
  395. package/src/renderables/__tests__/Markdown.code-colors.test.ts +0 -242
  396. package/src/renderables/__tests__/Markdown.test.ts +0 -2518
  397. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +0 -87
  398. package/src/renderables/__tests__/Textarea.buffer.test.ts +0 -682
  399. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +0 -675
  400. package/src/renderables/__tests__/Textarea.editing.test.ts +0 -2041
  401. package/src/renderables/__tests__/Textarea.error-handling.test.ts +0 -35
  402. package/src/renderables/__tests__/Textarea.events.test.ts +0 -738
  403. package/src/renderables/__tests__/Textarea.highlights.test.ts +0 -590
  404. package/src/renderables/__tests__/Textarea.keybinding.test.ts +0 -3149
  405. package/src/renderables/__tests__/Textarea.paste.test.ts +0 -357
  406. package/src/renderables/__tests__/Textarea.rendering.test.ts +0 -1866
  407. package/src/renderables/__tests__/Textarea.scroll.test.ts +0 -733
  408. package/src/renderables/__tests__/Textarea.selection.test.ts +0 -1590
  409. package/src/renderables/__tests__/Textarea.stress.test.ts +0 -670
  410. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +0 -383
  411. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +0 -310
  412. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +0 -221
  413. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +0 -89
  414. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +0 -457
  415. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +0 -158
  416. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +0 -387
  417. package/src/renderables/__tests__/markdown-parser.test.ts +0 -217
  418. package/src/renderables/__tests__/renderable-test-utils.ts +0 -60
  419. package/src/renderables/composition/README.md +0 -8
  420. package/src/renderables/composition/VRenderable.ts +0 -32
  421. package/src/renderables/composition/constructs.ts +0 -127
  422. package/src/renderables/composition/vnode.ts +0 -289
  423. package/src/renderables/index.ts +0 -23
  424. package/src/renderables/markdown-parser.ts +0 -66
  425. package/src/renderer.ts +0 -2681
  426. package/src/runtime-plugin-support.ts +0 -39
  427. package/src/runtime-plugin.ts +0 -615
  428. package/src/syntax-style.test.ts +0 -841
  429. package/src/syntax-style.ts +0 -257
  430. package/src/testing/README.md +0 -210
  431. package/src/testing/capture-spans.test.ts +0 -194
  432. package/src/testing/integration.test.ts +0 -276
  433. package/src/testing/manual-clock.ts +0 -117
  434. package/src/testing/mock-keys.test.ts +0 -1378
  435. package/src/testing/mock-keys.ts +0 -457
  436. package/src/testing/mock-mouse.test.ts +0 -218
  437. package/src/testing/mock-mouse.ts +0 -247
  438. package/src/testing/mock-tree-sitter-client.ts +0 -73
  439. package/src/testing/spy.ts +0 -13
  440. package/src/testing/test-recorder.test.ts +0 -415
  441. package/src/testing/test-recorder.ts +0 -145
  442. package/src/testing/test-renderer.ts +0 -132
  443. package/src/testing.ts +0 -7
  444. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +0 -481
  445. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +0 -19
  446. package/src/tests/__snapshots__/scrollbox.test.ts.snap +0 -29
  447. package/src/tests/absolute-positioning.snapshot.test.ts +0 -638
  448. package/src/tests/allocator-stats.test.ts +0 -38
  449. package/src/tests/destroy-during-render.test.ts +0 -200
  450. package/src/tests/destroy-on-exit.fixture.ts +0 -36
  451. package/src/tests/destroy-on-exit.test.ts +0 -41
  452. package/src/tests/hover-cursor.test.ts +0 -98
  453. package/src/tests/native-span-feed-async.test.ts +0 -173
  454. package/src/tests/native-span-feed-close.test.ts +0 -120
  455. package/src/tests/native-span-feed-coverage.test.ts +0 -227
  456. package/src/tests/native-span-feed-edge-cases.test.ts +0 -352
  457. package/src/tests/native-span-feed-use-after-free.test.ts +0 -45
  458. package/src/tests/opacity.test.ts +0 -123
  459. package/src/tests/renderable.snapshot.test.ts +0 -524
  460. package/src/tests/renderable.test.ts +0 -1281
  461. package/src/tests/renderer.clock.test.ts +0 -158
  462. package/src/tests/renderer.console-startup.test.ts +0 -185
  463. package/src/tests/renderer.control.test.ts +0 -425
  464. package/src/tests/renderer.core-slot-binding.test.ts +0 -952
  465. package/src/tests/renderer.cursor.test.ts +0 -26
  466. package/src/tests/renderer.destroy-during-render.test.ts +0 -147
  467. package/src/tests/renderer.focus-restore.test.ts +0 -257
  468. package/src/tests/renderer.focus.test.ts +0 -294
  469. package/src/tests/renderer.idle.test.ts +0 -219
  470. package/src/tests/renderer.input.test.ts +0 -2237
  471. package/src/tests/renderer.kitty-flags.test.ts +0 -195
  472. package/src/tests/renderer.mouse.test.ts +0 -1274
  473. package/src/tests/renderer.palette.test.ts +0 -629
  474. package/src/tests/renderer.selection.test.ts +0 -49
  475. package/src/tests/renderer.slot-registry.test.ts +0 -684
  476. package/src/tests/renderer.useMouse.test.ts +0 -47
  477. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +0 -76
  478. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +0 -43
  479. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +0 -67
  480. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +0 -72
  481. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +0 -44
  482. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +0 -85
  483. package/src/tests/runtime-plugin-path-alias.fixture.ts +0 -43
  484. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +0 -65
  485. package/src/tests/runtime-plugin-support.fixture.ts +0 -11
  486. package/src/tests/runtime-plugin-support.test.ts +0 -19
  487. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +0 -30
  488. package/src/tests/runtime-plugin.fixture.ts +0 -40
  489. package/src/tests/runtime-plugin.test.ts +0 -354
  490. package/src/tests/scrollbox-culling-bug.test.ts +0 -114
  491. package/src/tests/scrollbox-hitgrid-resize.test.ts +0 -136
  492. package/src/tests/scrollbox-hitgrid.test.ts +0 -909
  493. package/src/tests/scrollbox.test.ts +0 -1530
  494. package/src/tests/wrap-resize-perf.test.ts +0 -276
  495. package/src/tests/yoga-setters.test.ts +0 -921
  496. package/src/text-buffer-view.test.ts +0 -705
  497. package/src/text-buffer-view.ts +0 -189
  498. package/src/text-buffer.test.ts +0 -347
  499. package/src/text-buffer.ts +0 -250
  500. package/src/types.ts +0 -161
  501. package/src/utils.ts +0 -88
  502. package/src/zig/ansi.zig +0 -268
  503. package/src/zig/bench/README.md +0 -50
  504. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +0 -887
  505. package/src/zig/bench/edit-buffer_bench.zig +0 -476
  506. package/src/zig/bench/native-span-feed_bench.zig +0 -100
  507. package/src/zig/bench/rope-markers_bench.zig +0 -713
  508. package/src/zig/bench/rope_bench.zig +0 -514
  509. package/src/zig/bench/styled-text_bench.zig +0 -470
  510. package/src/zig/bench/text-buffer-coords_bench.zig +0 -362
  511. package/src/zig/bench/text-buffer-view_bench.zig +0 -459
  512. package/src/zig/bench/text-chunk-graphemes_bench.zig +0 -273
  513. package/src/zig/bench/utf8_bench.zig +0 -799
  514. package/src/zig/bench-utils.zig +0 -431
  515. package/src/zig/bench.zig +0 -217
  516. package/src/zig/buffer-methods.zig +0 -211
  517. package/src/zig/buffer.zig +0 -2281
  518. package/src/zig/build.zig +0 -289
  519. package/src/zig/build.zig.zon +0 -16
  520. package/src/zig/edit-buffer.zig +0 -825
  521. package/src/zig/editor-view.zig +0 -802
  522. package/src/zig/event-bus.zig +0 -13
  523. package/src/zig/event-emitter.zig +0 -65
  524. package/src/zig/file-logger.zig +0 -92
  525. package/src/zig/grapheme.zig +0 -599
  526. package/src/zig/lib.zig +0 -1854
  527. package/src/zig/link.zig +0 -333
  528. package/src/zig/logger.zig +0 -43
  529. package/src/zig/mem-registry.zig +0 -125
  530. package/src/zig/native-span-feed-bench-lib.zig +0 -7
  531. package/src/zig/native-span-feed.zig +0 -708
  532. package/src/zig/renderer.zig +0 -1393
  533. package/src/zig/rope.zig +0 -1220
  534. package/src/zig/syntax-style.zig +0 -161
  535. package/src/zig/terminal.zig +0 -987
  536. package/src/zig/test.zig +0 -72
  537. package/src/zig/tests/README.md +0 -18
  538. package/src/zig/tests/buffer-methods_test.zig +0 -1109
  539. package/src/zig/tests/buffer_test.zig +0 -2557
  540. package/src/zig/tests/edit-buffer-history_test.zig +0 -271
  541. package/src/zig/tests/edit-buffer_test.zig +0 -1689
  542. package/src/zig/tests/editor-view_test.zig +0 -3299
  543. package/src/zig/tests/event-emitter_test.zig +0 -249
  544. package/src/zig/tests/grapheme_test.zig +0 -1304
  545. package/src/zig/tests/link_test.zig +0 -190
  546. package/src/zig/tests/mem-registry_test.zig +0 -473
  547. package/src/zig/tests/memory_leak_regression_test.zig +0 -159
  548. package/src/zig/tests/native-span-feed_test.zig +0 -1264
  549. package/src/zig/tests/renderer_test.zig +0 -1017
  550. package/src/zig/tests/rope-nested_test.zig +0 -712
  551. package/src/zig/tests/rope_fuzz_test.zig +0 -238
  552. package/src/zig/tests/rope_test.zig +0 -2362
  553. package/src/zig/tests/segment-merge.test.zig +0 -148
  554. package/src/zig/tests/syntax-style_test.zig +0 -557
  555. package/src/zig/tests/terminal_test.zig +0 -754
  556. package/src/zig/tests/text-buffer-drawing_test.zig +0 -3237
  557. package/src/zig/tests/text-buffer-highlights_test.zig +0 -666
  558. package/src/zig/tests/text-buffer-iterators_test.zig +0 -776
  559. package/src/zig/tests/text-buffer-segment_test.zig +0 -320
  560. package/src/zig/tests/text-buffer-selection_test.zig +0 -1035
  561. package/src/zig/tests/text-buffer-selection_viewport_test.zig +0 -358
  562. package/src/zig/tests/text-buffer-view_test.zig +0 -3649
  563. package/src/zig/tests/text-buffer_test.zig +0 -2191
  564. package/src/zig/tests/unicode-width-map.zon +0 -3909
  565. package/src/zig/tests/utf8_no_zwj_test.zig +0 -260
  566. package/src/zig/tests/utf8_test.zig +0 -4057
  567. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +0 -267
  568. package/src/zig/tests/utf8_wcwidth_test.zig +0 -357
  569. package/src/zig/tests/word-wrap-editing_test.zig +0 -498
  570. package/src/zig/tests/wrap-cache-perf_test.zig +0 -113
  571. package/src/zig/text-buffer-iterators.zig +0 -499
  572. package/src/zig/text-buffer-segment.zig +0 -404
  573. package/src/zig/text-buffer-view.zig +0 -1371
  574. package/src/zig/text-buffer.zig +0 -1180
  575. package/src/zig/utf8.zig +0 -1948
  576. package/src/zig/utils.zig +0 -9
  577. package/src/zig-structs.ts +0 -261
  578. package/src/zig.ts +0 -3884
  579. package/tsconfig.build.json +0 -24
  580. package/tsconfig.json +0 -27
  581. /package/{src/lib/tree-sitter/assets → assets}/javascript/highlights.scm +0 -0
  582. /package/{src/lib/tree-sitter/assets → assets}/javascript/tree-sitter-javascript.wasm +0 -0
  583. /package/{src/lib/tree-sitter/assets → assets}/markdown/highlights.scm +0 -0
  584. /package/{src/lib/tree-sitter/assets → assets}/markdown/injections.scm +0 -0
  585. /package/{src/lib/tree-sitter/assets → assets}/markdown/tree-sitter-markdown.wasm +0 -0
  586. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/highlights.scm +0 -0
  587. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  588. /package/{src/lib/tree-sitter/assets → assets}/typescript/highlights.scm +0 -0
  589. /package/{src/lib/tree-sitter/assets → assets}/typescript/tree-sitter-typescript.wasm +0 -0
  590. /package/{src/lib/tree-sitter/assets → assets}/zig/highlights.scm +0 -0
  591. /package/{src/lib/tree-sitter/assets → assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -1,2660 +0,0 @@
1
- import { describe, expect, it, beforeEach, afterEach } from "bun:test"
2
- import { TextRenderable, type TextOptions } from "./Text.js"
3
- import { TextNodeRenderable } from "./TextNode.js"
4
- import { RGBA } from "../lib/RGBA.js"
5
- import { stringToStyledText, StyledText } from "../lib/styled-text.js"
6
- import { createTestRenderer, type MockMouse, type TestRenderer } from "../testing/test-renderer.js"
7
- import { BoxRenderable } from "./Box.js"
8
-
9
- let currentRenderer: TestRenderer
10
- let renderOnce: () => Promise<void>
11
- let currentMouse: MockMouse
12
- let captureFrame: () => string
13
- let resize: (width: number, height: number) => void
14
-
15
- async function createTextRenderable(
16
- renderer: TestRenderer,
17
- options: TextOptions,
18
- ): Promise<{ text: TextRenderable; root: any }> {
19
- const textRenderable = new TextRenderable(renderer, { left: 0, top: 0, ...options })
20
- renderer.root.add(textRenderable)
21
- await renderOnce()
22
-
23
- return { text: textRenderable, root: renderer.root }
24
- }
25
-
26
- describe("TextRenderable Selection", () => {
27
- describe("Native getSelectedText", () => {
28
- it("should use native implementation", async () => {
29
- const { text } = await createTextRenderable(currentRenderer, {
30
- content: "Hello World",
31
- selectable: true,
32
- })
33
-
34
- await currentMouse.drag(text.x, text.y, text.x + 5, text.y)
35
- await renderOnce()
36
-
37
- const selectedText = text.getSelectedText()
38
- expect(selectedText).toBe("Hello")
39
- })
40
-
41
- it("should handle graphemes correctly", async () => {
42
- const { text } = await createTextRenderable(currentRenderer, {
43
- content: "Hello 🌍 World",
44
- selectable: true,
45
- })
46
-
47
- // Select "Hello 🌍" (7 characters: H,e,l,l,o, ,🌍)
48
- await currentMouse.drag(text.x, text.y, text.x + 7, text.y)
49
- await renderOnce()
50
-
51
- const selectedText = text.getSelectedText()
52
- expect(selectedText).toBe("Hello 🌍")
53
- })
54
- })
55
-
56
- beforeEach(async () => {
57
- ;({
58
- renderer: currentRenderer,
59
- renderOnce,
60
- mockMouse: currentMouse,
61
- captureCharFrame: captureFrame,
62
- resize,
63
- } = await createTestRenderer({
64
- width: 20,
65
- height: 5,
66
- }))
67
- })
68
-
69
- afterEach(() => {
70
- currentRenderer.destroy()
71
- })
72
-
73
- describe("Initialization", () => {
74
- it("should initialize properly", async () => {
75
- const { text } = await createTextRenderable(currentRenderer, {
76
- content: "Hello World",
77
- selectable: true,
78
- })
79
-
80
- expect(text.x).toBeDefined()
81
- expect(text.y).toBeDefined()
82
- expect(text.width).toBeGreaterThan(0)
83
- expect(text.height).toBeGreaterThan(0)
84
- })
85
- })
86
-
87
- describe("Basic Selection Flow", () => {
88
- it("should handle selection from start to end", async () => {
89
- const { text } = await createTextRenderable(currentRenderer, {
90
- content: "Hello World",
91
- selectable: true,
92
- })
93
-
94
- expect(text.hasSelection()).toBe(false)
95
- expect(text.getSelection()).toBe(null)
96
- expect(text.getSelectedText()).toBe("")
97
-
98
- expect(text.shouldStartSelection(6, 0)).toBe(true)
99
-
100
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
101
- await renderOnce()
102
-
103
- expect(text.hasSelection()).toBe(true)
104
-
105
- const selection = text.getSelection()
106
- expect(selection).not.toBe(null)
107
- expect(selection!.start).toBe(6)
108
- expect(selection!.end).toBe(11)
109
-
110
- expect(text.getSelectedText()).toBe("World")
111
- })
112
-
113
- it("should handle selection with newline characters", async () => {
114
- const { text } = await createTextRenderable(currentRenderer, {
115
- content: "Line 1\nLine 2\nLine 3",
116
- selectable: true,
117
- })
118
-
119
- // Select from middle of line 2 to middle of line 3
120
- await currentMouse.drag(text.x + 2, text.y + 1, text.x + 4, text.y + 2)
121
- await renderOnce()
122
-
123
- const selection = text.getSelection()
124
- expect(selection).not.toBe(null)
125
- // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 starts at 7
126
- // Position "n" in "Line 2" is at 7 + 2 = 9
127
- expect(selection!.start).toBe(9)
128
- // Line 2 starts at 14, position after "Line" is 14 + 4 = 18
129
- expect(selection!.end).toBe(18)
130
-
131
- expect(text.getSelectedText()).toBe("ne 2\nLine")
132
- })
133
-
134
- it("should handle selection across empty lines", async () => {
135
- const { text } = await createTextRenderable(currentRenderer, {
136
- content: "Line 1\nLine 2\n\nLine 4",
137
- selectable: true,
138
- })
139
-
140
- // Select from start of line 1 to position 2 on empty line 3
141
- await currentMouse.drag(text.x, text.y, text.x + 2, text.y + 2)
142
- await renderOnce()
143
-
144
- const selection = text.getSelection()
145
- expect(selection).not.toBe(null)
146
- // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 (7-12) + newline (13) + Line 2 empty (14)
147
- // Selecting to (col=2, row=2) on empty line clamps to col=0, so end=14
148
- expect(selection!.start).toBe(0)
149
- expect(selection!.end).toBe(14)
150
- expect(text.getSelectedText()).toBe("Line 1\nLine 2")
151
- })
152
-
153
- it("should handle selection ending in empty line", async () => {
154
- const { text } = await createTextRenderable(currentRenderer, {
155
- content: "Line 1\n\nLine 3",
156
- selectable: true,
157
- })
158
-
159
- // Select from start of line 1 into the empty line 2
160
- await currentMouse.drag(text.x, text.y, text.x + 3, text.y + 1)
161
- await renderOnce()
162
-
163
- const selection = text.getSelection()
164
- expect(selection).not.toBe(null)
165
- // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 empty (7)
166
- // Selecting to (col=3, row=1) on empty line clamps to col=0, so end=7
167
- expect(selection!.start).toBe(0)
168
- expect(selection!.end).toBe(7)
169
- expect(text.getSelectedText()).toBe("Line 1")
170
- })
171
-
172
- it("should handle selection spanning multiple lines completely", async () => {
173
- const { text } = await createTextRenderable(currentRenderer, {
174
- content: "First\nSecond\nThird",
175
- selectable: true,
176
- })
177
-
178
- // Select from start of line 1 to end of line 2 (actually selecting Second)
179
- await currentMouse.drag(text.x, text.y + 1, text.x + 6, text.y + 1)
180
- await renderOnce()
181
-
182
- const selection = text.getSelection()
183
- expect(selection).not.toBe(null)
184
- expect(text.getSelectedText()).toBe("Second")
185
- })
186
-
187
- it("should handle selection including multiple line breaks", async () => {
188
- const { text } = await createTextRenderable(currentRenderer, {
189
- content: "A\nB\nC\nD",
190
- selectable: true,
191
- })
192
-
193
- // Select from middle of first line to middle of last line
194
- await currentMouse.drag(text.x, text.y + 1, text.x + 1, text.y + 2)
195
- await renderOnce()
196
-
197
- const selection = text.getSelection()
198
- expect(selection).not.toBe(null)
199
- const selectedText = text.getSelectedText()
200
- expect(selectedText).toContain("\n")
201
- expect(selectedText).toContain("B")
202
- expect(selectedText).toContain("C")
203
- })
204
-
205
- it("should handle selection that includes line breaks at boundaries", async () => {
206
- const { text } = await createTextRenderable(currentRenderer, {
207
- content: "Line1\nLine2\nLine3",
208
- selectable: true,
209
- })
210
-
211
- // Select across line boundaries
212
- await currentMouse.drag(text.x + 4, text.y, text.x + 2, text.y + 1)
213
- await renderOnce()
214
-
215
- const selection = text.getSelection()
216
- expect(selection).not.toBe(null)
217
- const selectedText = text.getSelectedText()
218
- expect(selectedText).toContain("1")
219
- expect(selectedText).toContain("\n")
220
- expect(selectedText).toContain("Li")
221
- })
222
-
223
- it("should handle reverse selection (end before start)", async () => {
224
- const { text } = await createTextRenderable(currentRenderer, {
225
- content: "Hello World",
226
- selectable: true,
227
- })
228
-
229
- await currentMouse.drag(text.x + 11, text.y, text.x + 6, text.y)
230
- await renderOnce()
231
-
232
- const selection = text.getSelection()
233
- expect(selection).not.toBe(null)
234
- expect(selection!.start).toBe(6)
235
- expect(selection!.end).toBe(11)
236
-
237
- expect(text.getSelectedText()).toBe("World")
238
- })
239
- })
240
-
241
- describe("Selection Edge Cases", () => {
242
- it("should handle empty text", async () => {
243
- const { text } = await createTextRenderable(currentRenderer, {
244
- content: "",
245
- selectable: true,
246
- })
247
-
248
- await currentMouse.drag(text.x, text.y, text.x, text.y)
249
- await renderOnce()
250
-
251
- expect(text.hasSelection()).toBe(false)
252
- expect(text.getSelection()).toBe(null)
253
- expect(text.getSelectedText()).toBe("")
254
- })
255
-
256
- it("should handle single character selection", async () => {
257
- const { text } = await createTextRenderable(currentRenderer, {
258
- content: "A",
259
- selectable: true,
260
- })
261
-
262
- await currentMouse.drag(text.x, text.y, text.x + 1, text.y)
263
- await renderOnce()
264
-
265
- const selection = text.getSelection()
266
- expect(selection).not.toBe(null)
267
- expect(selection!.start).toBe(0)
268
- expect(selection!.end).toBe(1)
269
-
270
- expect(text.getSelectedText()).toBe("A")
271
- })
272
-
273
- it("should handle zero-width selection", async () => {
274
- const { text } = await createTextRenderable(currentRenderer, {
275
- content: "Hello World",
276
- selectable: true,
277
- })
278
-
279
- await currentMouse.drag(text.x + 5, text.y, text.x + 5, text.y)
280
- await renderOnce()
281
-
282
- expect(text.hasSelection()).toBe(false)
283
- expect(text.getSelection()).toBe(null)
284
- expect(text.getSelectedText()).toBe("")
285
- })
286
-
287
- it("should handle selection beyond text bounds", async () => {
288
- const { text } = await createTextRenderable(currentRenderer, {
289
- content: "Hi",
290
- selectable: true,
291
- })
292
-
293
- await currentMouse.drag(text.x, text.y, text.x + 10, text.y)
294
- await renderOnce()
295
-
296
- const selection = text.getSelection()
297
- expect(selection).not.toBe(null)
298
- expect(selection!.start).toBe(0)
299
- expect(selection!.end).toBe(2)
300
-
301
- expect(text.getSelectedText()).toBe("Hi")
302
- })
303
- })
304
-
305
- describe("Selection with Styled Text", () => {
306
- it("should handle styled text selection", async () => {
307
- const styledText = stringToStyledText("Hello World")
308
- styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text
309
-
310
- const { text } = await createTextRenderable(currentRenderer, {
311
- content: styledText,
312
- selectable: true,
313
- })
314
-
315
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
316
- await renderOnce()
317
-
318
- const selection = text.getSelection()
319
- expect(selection).not.toBe(null)
320
- expect(selection!.start).toBe(6)
321
- expect(selection!.end).toBe(11)
322
-
323
- expect(text.getSelectedText()).toBe("World")
324
- })
325
-
326
- it("should handle selection with different text colors", async () => {
327
- const { text } = await createTextRenderable(currentRenderer, {
328
- content: "Red and Blue",
329
- selectable: true,
330
- selectionBg: RGBA.fromValues(1, 1, 0, 1),
331
- selectionFg: RGBA.fromValues(0, 0, 0, 1),
332
- })
333
-
334
- await currentMouse.drag(text.x + 8, text.y, text.x + 12, text.y)
335
- await renderOnce()
336
-
337
- const selection = text.getSelection()
338
- expect(selection).not.toBe(null)
339
- expect(selection!.start).toBe(8)
340
- expect(selection!.end).toBe(12)
341
-
342
- expect(text.getSelectedText()).toBe("Blue")
343
- })
344
- })
345
-
346
- describe("Selection State Management", () => {
347
- it("should clear selection when selection is cleared", async () => {
348
- const { text } = await createTextRenderable(currentRenderer, {
349
- content: "Hello World",
350
- selectable: true,
351
- })
352
-
353
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
354
- await renderOnce()
355
- expect(text.hasSelection()).toBe(true)
356
-
357
- currentRenderer.clearSelection()
358
- await renderOnce()
359
-
360
- expect(text.hasSelection()).toBe(false)
361
- expect(text.getSelection()).toBe(null)
362
- expect(text.getSelectedText()).toBe("")
363
- })
364
-
365
- it("should handle multiple selection changes", async () => {
366
- const { text } = await createTextRenderable(currentRenderer, {
367
- content: "Hello World Test",
368
- selectable: true,
369
- })
370
-
371
- await currentMouse.drag(text.x + 0, text.y, text.x + 5, text.y)
372
- await renderOnce()
373
- expect(text.getSelectedText()).toBe("Hello")
374
- expect(text.getSelection()).toEqual({ start: 0, end: 5 })
375
-
376
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
377
- await renderOnce()
378
- expect(text.getSelectedText()).toBe("World")
379
- expect(text.getSelection()).toEqual({ start: 6, end: 11 })
380
-
381
- await currentMouse.drag(text.x + 12, text.y, text.x + 16, text.y)
382
- await renderOnce()
383
- expect(text.getSelectedText()).toBe("Test")
384
- expect(text.getSelection()).toEqual({ start: 12, end: 16 })
385
- })
386
- })
387
-
388
- describe("shouldStartSelection", () => {
389
- it("should return false for non-selectable text", async () => {
390
- const { text } = await createTextRenderable(currentRenderer, {
391
- content: "Hello World",
392
- selectable: false,
393
- })
394
-
395
- expect(text.shouldStartSelection(0, 0)).toBe(false)
396
- expect(text.shouldStartSelection(5, 0)).toBe(false)
397
- })
398
-
399
- it("should return true for selectable text within bounds", async () => {
400
- const { text } = await createTextRenderable(currentRenderer, {
401
- content: "Hello World",
402
- selectable: true,
403
- })
404
-
405
- expect(text.shouldStartSelection(0, 0)).toBe(true) // Start of text
406
- expect(text.shouldStartSelection(5, 0)).toBe(true) // Middle of text
407
- expect(text.shouldStartSelection(10, 0)).toBe(true) // End of text
408
- })
409
-
410
- it("should handle shouldStartSelection with multi-line text", async () => {
411
- const { text } = await createTextRenderable(currentRenderer, {
412
- content: "Line 1\nLine 2\nLine 3",
413
- selectable: true,
414
- })
415
-
416
- expect(text.shouldStartSelection(0, 0)).toBe(true) // Line 1 start
417
- expect(text.shouldStartSelection(2, 1)).toBe(true) // Line 2 middle
418
- expect(text.shouldStartSelection(5, 2)).toBe(true) // Line 3 end
419
- })
420
- })
421
-
422
- describe("Selection with Custom Dimensions", () => {
423
- it("should handle selection in constrained width", async () => {
424
- const { text } = await createTextRenderable(currentRenderer, {
425
- content: "This is a very long text that should wrap to multiple lines",
426
- width: 10,
427
- selectable: true,
428
- })
429
-
430
- await currentMouse.drag(text.x, text.y, text.x + 10, text.y + 2)
431
- await renderOnce()
432
-
433
- const selection = text.getSelection()
434
- expect(selection).not.toBe(null)
435
- expect(selection!.start).toBeGreaterThanOrEqual(0)
436
- expect(selection!.end).toBeGreaterThan(selection!.start)
437
- expect(text.getSelectedText().length).toBeGreaterThan(0)
438
- })
439
- })
440
-
441
- describe("Cross-Renderable Selection in Nested Boxes", () => {
442
- it("should handle selection across multiple nested text renderables in boxes", async () => {
443
- const { text: statusText } = await createTextRenderable(currentRenderer, {
444
- content: "Selected 5 chars:",
445
- selectable: true,
446
- fg: "#f0f6fc",
447
- top: 0,
448
- })
449
-
450
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
451
- content: '"Hello"',
452
- selectable: true,
453
- fg: "#7dd3fc",
454
- top: 1,
455
- })
456
-
457
- const { text: selectionMiddleText } = await createTextRenderable(currentRenderer, {
458
- content: "",
459
- selectable: true,
460
- fg: "#94a3b8",
461
- top: 2,
462
- })
463
-
464
- const { text: selectionEndText } = await createTextRenderable(currentRenderer, {
465
- content: "",
466
- selectable: true,
467
- fg: "#7dd3fc",
468
- top: 3,
469
- })
470
-
471
- const { text: debugText } = await createTextRenderable(currentRenderer, {
472
- content: "Selected renderables: 2/5",
473
- selectable: true,
474
- fg: "#e6edf3",
475
- top: 4,
476
- })
477
-
478
- // Simulate starting selection above the box and ending below/right of the box
479
- // This should cover all renderables in the "box"
480
- const allRenderables = [statusText, selectionStartText, selectionMiddleText, selectionEndText, debugText]
481
-
482
- await currentMouse.drag(0, 0, 50, 10)
483
- await renderOnce()
484
-
485
- expect(statusText.hasSelection()).toBe(true)
486
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
487
-
488
- expect(selectionStartText.hasSelection()).toBe(true)
489
- expect(selectionStartText.getSelectedText()).toBe('"Hello"')
490
-
491
- // Empty text renderables should not have selections since there's no content to select
492
- expect(selectionMiddleText.hasSelection()).toBe(false)
493
- expect(selectionMiddleText.getSelectedText()).toBe("")
494
-
495
- expect(selectionEndText.hasSelection()).toBe(false)
496
- expect(selectionEndText.getSelectedText()).toBe("")
497
-
498
- expect(debugText.hasSelection()).toBe(true)
499
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
500
-
501
- const globalSelectedText = currentRenderer.getSelection()?.getSelectedText()
502
-
503
- expect(globalSelectedText).toContain("Selected 5 chars:")
504
- expect(globalSelectedText).toContain('"Hello"')
505
- expect(globalSelectedText).toContain("Selected renderables: 2/5")
506
- })
507
-
508
- it("should automatically update selection when text content changes within covered area", async () => {
509
- const { text: statusText } = await createTextRenderable(currentRenderer, {
510
- content: "Selected 5 chars:",
511
- selectable: true,
512
- fg: "#f0f6fc",
513
- top: 0,
514
- wrapMode: "none",
515
- })
516
-
517
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
518
- top: 1,
519
- content: '"Hello"',
520
- selectable: true,
521
- fg: "#7dd3fc",
522
- wrapMode: "none",
523
- })
524
-
525
- const { text: debugText } = await createTextRenderable(currentRenderer, {
526
- top: 2,
527
- content: "Selected renderables: 2/5",
528
- selectable: true,
529
- fg: "#e6edf3",
530
- wrapMode: "none",
531
- })
532
-
533
- await currentMouse.drag(0, 0, 50, 5)
534
- await renderOnce()
535
-
536
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
537
- expect(selectionStartText.getSelectedText()).toBe('"Hello"')
538
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
539
-
540
- selectionStartText.content = '"Hello World Extended Selection"'
541
-
542
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
543
- expect(selectionStartText.getSelectedText()).toBe('"Hello World Extended Selection"')
544
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
545
-
546
- const updatedGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
547
-
548
- expect(updatedGlobalSelectedText).toContain('"Hello World Extended Selection"')
549
- expect(updatedGlobalSelectedText).toContain("Selected 5 chars:")
550
- expect(updatedGlobalSelectedText).toContain("Selected renderables: 2/5")
551
-
552
- debugText.content = "Selected renderables: 3/5 | Container: statusBox"
553
-
554
- expect(debugText.getSelectedText()).toBe("Selected renderables: 3/5 | Container: statusBox")
555
-
556
- const finalGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
557
-
558
- expect(finalGlobalSelectedText).toContain("Selected renderables: 3/5 | Container: statusBox")
559
- })
560
-
561
- it("should automatically update selection when text node content changes with clear and add", async () => {
562
- const { text: statusText } = await createTextRenderable(currentRenderer, {
563
- content: "",
564
- selectable: true,
565
- fg: "#f0f6fc",
566
- top: 0,
567
- wrapMode: "none",
568
- })
569
-
570
- const statusNode = new TextNodeRenderable({})
571
- statusNode.add("Selected 5 chars:")
572
- statusText.add(statusNode)
573
-
574
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
575
- content: "",
576
- selectable: true,
577
- fg: "#7dd3fc",
578
- top: 1,
579
- wrapMode: "none",
580
- })
581
-
582
- const selectionNode = new TextNodeRenderable({})
583
- selectionNode.add('"Hello"')
584
- selectionStartText.add(selectionNode)
585
-
586
- const { text: debugText } = await createTextRenderable(currentRenderer, {
587
- content: "",
588
- selectable: true,
589
- fg: "#e6edf3",
590
- top: 2,
591
- wrapMode: "none",
592
- })
593
-
594
- const debugNode = new TextNodeRenderable({})
595
- debugNode.add("Selected renderables: 2/5")
596
- debugText.add(debugNode)
597
-
598
- await currentMouse.drag(0, 0, 50, 5)
599
- await renderOnce()
600
-
601
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
602
- expect(selectionStartText.getSelectedText()).toBe('"Hello"')
603
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
604
-
605
- // Clear and add new content to the selection node
606
- selectionNode.clear()
607
- selectionNode.add('"Hello World Extended Selection"')
608
- await renderOnce()
609
-
610
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
611
- expect(selectionStartText.getSelectedText()).toBe('"Hello World Extended Selection"')
612
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
613
-
614
- const updatedGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
615
-
616
- expect(updatedGlobalSelectedText).toContain('"Hello World Extended Selection"')
617
- expect(updatedGlobalSelectedText).toContain("Selected 5 chars:")
618
- expect(updatedGlobalSelectedText).toContain("Selected renderables: 2/5")
619
-
620
- // Clear and add new content to the debug node
621
- debugNode.clear()
622
- debugNode.add("Selected renderables: 3/5 | Container: statusBox")
623
- await renderOnce()
624
-
625
- expect(debugText.getSelectedText()).toBe("Selected renderables: 3/5 | Container: statusBox")
626
-
627
- const finalGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
628
-
629
- expect(finalGlobalSelectedText).toContain("Selected renderables: 3/5 | Container: statusBox")
630
- })
631
-
632
- it("should handle selection that starts above box and ends below/right of box", async () => {
633
- const { text: statusText } = await createTextRenderable(currentRenderer, {
634
- content: "Status: Selection active",
635
- selectable: true,
636
- fg: "#f0f6fc",
637
- top: 2,
638
- wrapMode: "none",
639
- })
640
-
641
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
642
- content: "Start: (10,5)",
643
- selectable: true,
644
- fg: "#7dd3fc",
645
- top: 3,
646
- wrapMode: "none",
647
- })
648
-
649
- const { text: selectionEndText } = await createTextRenderable(currentRenderer, {
650
- content: "End: (45,12)",
651
- selectable: true,
652
- fg: "#7dd3fc",
653
- top: 4,
654
- wrapMode: "none",
655
- })
656
-
657
- const { text: debugText } = await createTextRenderable(currentRenderer, {
658
- content: "Debug: Cross-renderable selection spanning 3 elements",
659
- selectable: true,
660
- fg: "#e6edf3",
661
- top: 5,
662
- wrapMode: "none",
663
- })
664
-
665
- const allRenderables = [statusText, selectionStartText, selectionEndText, debugText]
666
-
667
- await currentMouse.drag(statusText.x, statusText.y, 60, 10)
668
- await renderOnce()
669
-
670
- allRenderables.forEach((renderable) => {
671
- expect(renderable.hasSelection()).toBe(true)
672
- })
673
-
674
- expect(statusText.getSelectedText()).toBe("Status: Selection active")
675
- expect(selectionStartText.getSelectedText()).toBe("Start: (10,5)")
676
- expect(selectionEndText.getSelectedText()).toBe("End: (45,12)")
677
- expect(debugText.getSelectedText()).toBe("Debug: Cross-renderable selection spanning 3 elements")
678
-
679
- const globalSelectedText = currentRenderer.getSelection()?.getSelectedText()
680
-
681
- expect(globalSelectedText).toContain("Status: Selection active")
682
- expect(globalSelectedText).toContain("Start: (10,5)")
683
- expect(globalSelectedText).toContain("End: (45,12)")
684
- expect(globalSelectedText).toContain("Debug: Cross-renderable selection spanning 3 elements")
685
- })
686
- })
687
-
688
- describe("TextNode Integration with getPlainText", () => {
689
- it("should render correct plain text after adding TextNodes", async () => {
690
- const { text, root } = await createTextRenderable(currentRenderer, {
691
- content: "",
692
- selectable: true,
693
- })
694
-
695
- const node1 = new TextNodeRenderable({
696
- fg: RGBA.fromValues(1, 0, 0, 1),
697
- bg: RGBA.fromValues(0, 0, 0, 1),
698
- })
699
- node1.add("Hello")
700
-
701
- const node2 = new TextNodeRenderable({
702
- fg: RGBA.fromValues(0, 1, 0, 1),
703
- bg: RGBA.fromValues(0, 0, 0, 1),
704
- })
705
- node2.add(" World")
706
-
707
- text.add(node1)
708
- text.add(node2)
709
-
710
- await renderOnce()
711
-
712
- expect(text.plainText).toBe("Hello World")
713
- })
714
-
715
- it("should render correct plain text after inserting TextNodes", async () => {
716
- const { text, root } = await createTextRenderable(currentRenderer, {
717
- content: "",
718
- selectable: true,
719
- })
720
-
721
- const node1 = new TextNodeRenderable({})
722
- node1.add("Hello")
723
-
724
- const node2 = new TextNodeRenderable({})
725
- node2.add(" World")
726
-
727
- const node3 = new TextNodeRenderable({})
728
- node3.add("!")
729
-
730
- text.add(node1)
731
- text.add(node2)
732
-
733
- text.insertBefore(node3, node2)
734
-
735
- await renderOnce()
736
-
737
- expect(text.plainText).toBe("Hello! World")
738
- })
739
-
740
- it("should render correct plain text after removing TextNodes", async () => {
741
- const { text, root } = await createTextRenderable(currentRenderer, {
742
- content: "",
743
- selectable: true,
744
- })
745
-
746
- const node1 = new TextNodeRenderable({})
747
- node1.add("Hello")
748
-
749
- const node2 = new TextNodeRenderable({})
750
- node2.add(" Cruel")
751
-
752
- const node3 = new TextNodeRenderable({})
753
- node3.add(" World")
754
-
755
- text.add(node1)
756
- text.add(node2)
757
- text.add(node3)
758
-
759
- await renderOnce()
760
- expect(text.plainText).toBe("Hello Cruel World")
761
-
762
- text.remove(node2.id)
763
-
764
- await renderOnce()
765
-
766
- expect(text.plainText).toBe("Hello World")
767
- })
768
-
769
- it("should handle simple add and remove operations", async () => {
770
- const { text, root } = await createTextRenderable(currentRenderer, {
771
- content: "",
772
- selectable: true,
773
- })
774
-
775
- const node = new TextNodeRenderable({})
776
- node.add("Test")
777
-
778
- text.add(node)
779
-
780
- await renderOnce()
781
- expect(text.plainText).toBe("Test")
782
-
783
- text.remove(node.id)
784
-
785
- await renderOnce()
786
- expect(text.plainText).toBe("")
787
- })
788
-
789
- it("should render correct plain text after clearing all TextNodes", async () => {
790
- const { text, root } = await createTextRenderable(currentRenderer, {
791
- content: "",
792
- selectable: true,
793
- })
794
-
795
- const node1 = new TextNodeRenderable({})
796
- node1.add("Hello")
797
-
798
- const node2 = new TextNodeRenderable({})
799
- node2.add(" World")
800
-
801
- text.add(node1)
802
- text.add(node2)
803
-
804
- await renderOnce()
805
- expect(text.plainText).toBe("Hello World")
806
-
807
- text.clear()
808
-
809
- await renderOnce()
810
-
811
- expect(text.plainText).toBe("")
812
- })
813
-
814
- it("should handle nested TextNode structures correctly", async () => {
815
- const { text, root } = await createTextRenderable(currentRenderer, {
816
- content: "",
817
- selectable: true,
818
- })
819
-
820
- // Create nested structure: Parent -> [Child1, Child2]
821
- const parent = new TextNodeRenderable({
822
- fg: RGBA.fromValues(1, 1, 0, 1),
823
- })
824
-
825
- const child1 = new TextNodeRenderable({
826
- fg: RGBA.fromValues(1, 0, 0, 1),
827
- })
828
- child1.add("Red")
829
-
830
- const child2 = new TextNodeRenderable({
831
- fg: RGBA.fromValues(0, 1, 0, 1),
832
- })
833
- child2.add(" Green")
834
-
835
- parent.add(child1)
836
- parent.add(child2)
837
-
838
- const standalone = new TextNodeRenderable({
839
- fg: RGBA.fromValues(0, 0, 1, 1),
840
- })
841
- standalone.add(" Blue")
842
-
843
- text.add(parent)
844
- text.add(standalone)
845
-
846
- await renderOnce()
847
-
848
- expect(text.plainText).toBe("Red Green Blue")
849
- })
850
-
851
- it("should handle mixed string and TextNode content", async () => {
852
- const { text, root } = await createTextRenderable(currentRenderer, {
853
- content: "",
854
- selectable: true,
855
- })
856
-
857
- const startNode = new TextNodeRenderable({})
858
- startNode.add("Start ")
859
-
860
- const node1 = new TextNodeRenderable({})
861
- node1.add("middle")
862
-
863
- const node2 = new TextNodeRenderable({})
864
- node2.add(" end")
865
-
866
- text.add(startNode)
867
- text.add(node1)
868
- text.add(node2)
869
-
870
- await renderOnce()
871
-
872
- expect(text.plainText).toBe("Start middle end")
873
- })
874
-
875
- it("should handle TextNode operations with inherited styles", async () => {
876
- const { text, root } = await createTextRenderable(currentRenderer, {
877
- content: "",
878
- selectable: true,
879
- fg: RGBA.fromValues(1, 1, 1, 1), // White default
880
- })
881
-
882
- const redParent = new TextNodeRenderable({
883
- fg: RGBA.fromValues(1, 0, 0, 1), // Red
884
- })
885
-
886
- const redChild = new TextNodeRenderable({})
887
-
888
- const greenGrandchild = new TextNodeRenderable({
889
- fg: RGBA.fromValues(0, 1, 0, 1), // Green
890
- })
891
- greenGrandchild.add("Green")
892
-
893
- redChild.add(greenGrandchild)
894
- redParent.add(redChild)
895
-
896
- const blueNode = new TextNodeRenderable({
897
- fg: RGBA.fromValues(0, 0, 1, 1), // Blue
898
- })
899
- blueNode.add(" Blue")
900
-
901
- text.add(redParent)
902
- text.add(blueNode)
903
-
904
- await renderOnce()
905
-
906
- expect(text.plainText).toBe("Green Blue")
907
- })
908
-
909
- it("should handle empty TextNodes correctly", async () => {
910
- const { text, root } = await createTextRenderable(currentRenderer, {
911
- content: "",
912
- selectable: true,
913
- })
914
-
915
- const emptyNode1 = new TextNodeRenderable({})
916
- const nodeWithText = new TextNodeRenderable({})
917
- nodeWithText.add("Text")
918
- const emptyNode2 = new TextNodeRenderable({})
919
-
920
- text.add(emptyNode1)
921
- text.add(nodeWithText)
922
- text.add(emptyNode2)
923
-
924
- await renderOnce()
925
-
926
- expect(text.plainText).toBe("Text")
927
- })
928
-
929
- it("should handle complex TextNode operations sequence", async () => {
930
- const { text, root } = await createTextRenderable(currentRenderer, {
931
- content: "",
932
- selectable: true,
933
- })
934
-
935
- const initialNode = new TextNodeRenderable({})
936
- initialNode.add("Initial")
937
-
938
- const nodeA = new TextNodeRenderable({})
939
- nodeA.add(" A")
940
-
941
- const nodeB = new TextNodeRenderable({})
942
- nodeB.add(" B")
943
-
944
- const nodeC = new TextNodeRenderable({})
945
- nodeC.add(" C")
946
-
947
- const nodeD = new TextNodeRenderable({})
948
- nodeD.add(" D")
949
-
950
- text.add(initialNode)
951
- text.add(nodeA)
952
- text.add(nodeB)
953
- text.add(nodeC)
954
- text.add(nodeD)
955
-
956
- await renderOnce()
957
- expect(text.plainText).toBe("Initial A B C D")
958
-
959
- text.remove(nodeB.id)
960
-
961
- await renderOnce()
962
- expect(text.plainText).toBe("Initial A C D")
963
-
964
- const nodeX = new TextNodeRenderable({})
965
- nodeX.add(" X")
966
- text.insertBefore(nodeX, nodeC)
967
-
968
- await renderOnce()
969
- expect(text.plainText).toBe("Initial A X C D")
970
-
971
- nodeX.add(" Y")
972
-
973
- await renderOnce()
974
- expect(text.plainText).toBe("Initial A X Y C D")
975
- })
976
-
977
- it("should inherit fg/bg colors from TextRenderable to TextNode children", async () => {
978
- const { text, root } = await createTextRenderable(currentRenderer, {
979
- content: "",
980
- selectable: true,
981
- fg: RGBA.fromValues(1, 0, 0, 1),
982
- bg: RGBA.fromValues(0, 0, 1, 1),
983
- })
984
-
985
- const child1 = new TextNodeRenderable({})
986
- child1.add("Child1")
987
-
988
- const child2 = new TextNodeRenderable({})
989
- child2.add(" Child2")
990
-
991
- text.add(child1)
992
- text.add(child2)
993
-
994
- await renderOnce()
995
-
996
- expect(text.plainText).toBe("Child1 Child2")
997
-
998
- const chunks = text.textNode.gatherWithInheritedStyle()
999
-
1000
- expect(chunks).toHaveLength(2)
1001
-
1002
- chunks.forEach((chunk) => {
1003
- expect(chunk.fg).toEqual(RGBA.fromValues(1, 0, 0, 1))
1004
- expect(chunk.bg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1005
- expect(chunk.attributes).toBe(0)
1006
- })
1007
-
1008
- expect(chunks[0].text).toBe("Child1")
1009
- expect(chunks[1].text).toBe(" Child2")
1010
- })
1011
-
1012
- it("should allow TextNode children to override parent TextRenderable colors", async () => {
1013
- const { text, root } = await createTextRenderable(currentRenderer, {
1014
- content: "",
1015
- selectable: true,
1016
- fg: RGBA.fromValues(1, 0, 0, 1),
1017
- bg: RGBA.fromValues(0, 0, 1, 1),
1018
- })
1019
-
1020
- const inheritingChild = new TextNodeRenderable({})
1021
- inheritingChild.add("Inherit")
1022
-
1023
- const overridingChild = new TextNodeRenderable({
1024
- fg: RGBA.fromValues(0, 1, 0, 1),
1025
- bg: RGBA.fromValues(1, 1, 0, 1),
1026
- })
1027
- overridingChild.add(" Override")
1028
-
1029
- const partialOverrideChild = new TextNodeRenderable({
1030
- fg: RGBA.fromValues(0, 0, 1, 1),
1031
- })
1032
- partialOverrideChild.add(" Partial")
1033
-
1034
- text.add(inheritingChild)
1035
- text.add(overridingChild)
1036
- text.add(partialOverrideChild)
1037
-
1038
- await renderOnce()
1039
-
1040
- expect(text.plainText).toBe("Inherit Override Partial")
1041
-
1042
- const chunks = text.textNode.gatherWithInheritedStyle()
1043
-
1044
- expect(chunks).toHaveLength(3)
1045
-
1046
- // First child: inherits both fg and bg from parent
1047
- expect(chunks[0].text).toBe("Inherit")
1048
- expect(chunks[0].fg).toEqual(RGBA.fromValues(1, 0, 0, 1))
1049
- expect(chunks[0].bg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1050
-
1051
- // Second child: overrides both fg and bg
1052
- expect(chunks[1].text).toBe(" Override")
1053
- expect(chunks[1].fg).toEqual(RGBA.fromValues(0, 1, 0, 1))
1054
- expect(chunks[1].bg).toEqual(RGBA.fromValues(1, 1, 0, 1))
1055
-
1056
- // Third child: overrides fg, inherits bg
1057
- expect(chunks[2].text).toBe(" Partial")
1058
- expect(chunks[2].fg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1059
- expect(chunks[2].bg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1060
- })
1061
-
1062
- it("should inherit TextRenderable colors through nested TextNode hierarchies", async () => {
1063
- const { text, root } = await createTextRenderable(currentRenderer, {
1064
- content: "",
1065
- selectable: true,
1066
- fg: RGBA.fromValues(0, 1, 0, 1),
1067
- bg: RGBA.fromValues(0, 0, 0, 1),
1068
- })
1069
-
1070
- const grandparent = new TextNodeRenderable({})
1071
- const parent = new TextNodeRenderable({})
1072
- const child = new TextNodeRenderable({})
1073
-
1074
- child.add("Deep")
1075
- parent.add("Nested ")
1076
- parent.add(child)
1077
- grandparent.add("Very ")
1078
- grandparent.add(parent)
1079
-
1080
- text.add(grandparent)
1081
-
1082
- await renderOnce()
1083
-
1084
- expect(text.plainText).toBe("Very Nested Deep")
1085
-
1086
- const chunks = text.textNode.gatherWithInheritedStyle()
1087
-
1088
- expect(chunks).toHaveLength(3)
1089
-
1090
- // All chunks should inherit the TextRenderable's green fg and black bg
1091
- chunks.forEach((chunk) => {
1092
- expect(chunk.fg).toEqual(RGBA.fromValues(0, 1, 0, 1))
1093
- expect(chunk.bg).toEqual(RGBA.fromValues(0, 0, 0, 1))
1094
- expect(chunk.attributes).toBe(0)
1095
- })
1096
-
1097
- expect(chunks[0].text).toBe("Very ")
1098
- expect(chunks[1].text).toBe("Nested ")
1099
- expect(chunks[2].text).toBe("Deep")
1100
- })
1101
-
1102
- it("should handle TextRenderable color changes affecting existing TextNode children", async () => {
1103
- const { text, root } = await createTextRenderable(currentRenderer, {
1104
- content: "",
1105
- selectable: true,
1106
- fg: RGBA.fromValues(1, 0, 0, 1),
1107
- bg: RGBA.fromValues(0, 0, 0, 1),
1108
- })
1109
-
1110
- const child1 = new TextNodeRenderable({})
1111
- child1.add("Before")
1112
-
1113
- const child2 = new TextNodeRenderable({})
1114
- child2.add(" Change")
1115
-
1116
- text.add(child1)
1117
- text.add(child2)
1118
-
1119
- await renderOnce()
1120
- expect(text.plainText).toBe("Before Change")
1121
-
1122
- text.fg = RGBA.fromValues(0, 0, 1, 1)
1123
- text.bg = RGBA.fromValues(1, 1, 1, 1)
1124
-
1125
- await renderOnce()
1126
-
1127
- const chunks = text.textNode.gatherWithInheritedStyle()
1128
-
1129
- expect(chunks).toHaveLength(2)
1130
-
1131
- chunks.forEach((chunk) => {
1132
- expect(chunk.fg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1133
- expect(chunk.bg).toEqual(RGBA.fromValues(1, 1, 1, 1))
1134
- })
1135
-
1136
- expect(chunks[0].text).toBe("Before")
1137
- expect(chunks[1].text).toBe(" Change")
1138
- })
1139
-
1140
- it("should handle TextNode commands with multiple operations per render", async () => {
1141
- const { text, root } = await createTextRenderable(currentRenderer, {
1142
- content: "",
1143
- selectable: true,
1144
- })
1145
-
1146
- const node1 = new TextNodeRenderable({})
1147
- node1.add("First")
1148
-
1149
- const node2 = new TextNodeRenderable({})
1150
- node2.add("Second")
1151
-
1152
- const node3 = new TextNodeRenderable({})
1153
- node3.add("Third")
1154
-
1155
- text.add(node1)
1156
- text.add(node2)
1157
- text.insertBefore(node3, node1)
1158
-
1159
- node2.add(" Modified")
1160
-
1161
- await renderOnce()
1162
-
1163
- expect(text.plainText).toBe("ThirdFirstSecond Modified")
1164
- })
1165
- })
1166
-
1167
- describe("StyledText Integration", () => {
1168
- it("should render StyledText content correctly", async () => {
1169
- const styledText = stringToStyledText("Hello World")
1170
-
1171
- styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text
1172
- styledText.chunks[0].bg = RGBA.fromValues(0, 0, 0, 1) // Black background
1173
-
1174
- const { text } = await createTextRenderable(currentRenderer, {
1175
- content: styledText,
1176
- selectable: true,
1177
- })
1178
-
1179
- await renderOnce()
1180
-
1181
- expect(text.plainText).toBe("Hello World")
1182
- expect(text.width).toBeGreaterThan(0)
1183
- expect(text.height).toBeGreaterThan(0)
1184
- })
1185
-
1186
- it("should handle selection with StyledText content", async () => {
1187
- const styledText = stringToStyledText("Hello World")
1188
- styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text
1189
-
1190
- const { text } = await createTextRenderable(currentRenderer, {
1191
- content: styledText,
1192
- selectable: true,
1193
- })
1194
-
1195
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
1196
- await renderOnce()
1197
-
1198
- const selection = text.getSelection()
1199
- expect(selection).not.toBe(null)
1200
- expect(selection!.start).toBe(6)
1201
- expect(selection!.end).toBe(11)
1202
- expect(text.getSelectedText()).toBe("World")
1203
- })
1204
-
1205
- it("should handle empty StyledText", async () => {
1206
- const emptyStyledText = stringToStyledText("")
1207
-
1208
- const { text, root } = await createTextRenderable(currentRenderer, {
1209
- content: emptyStyledText,
1210
- selectable: true,
1211
- })
1212
-
1213
- await renderOnce()
1214
-
1215
- expect(text.plainText).toBe("")
1216
- expect(text.hasSelection()).toBe(false)
1217
- expect(text.getSelectedText()).toBe("")
1218
- })
1219
-
1220
- it("should handle StyledText with multiple chunks", async () => {
1221
- const styledText = new StyledText([
1222
- { __isChunk: true, text: "Red", fg: RGBA.fromValues(1, 0, 0, 1), attributes: 1 },
1223
- { __isChunk: true, text: " ", fg: undefined, attributes: 0 },
1224
- { __isChunk: true, text: "Green", fg: RGBA.fromValues(0, 1, 0, 1), attributes: 2 },
1225
- { __isChunk: true, text: " ", fg: undefined, attributes: 0 },
1226
- { __isChunk: true, text: "Blue", fg: RGBA.fromValues(0, 0, 1, 1), attributes: 0 },
1227
- ])
1228
-
1229
- const { text } = await createTextRenderable(currentRenderer, {
1230
- content: styledText,
1231
- selectable: true,
1232
- })
1233
-
1234
- await renderOnce()
1235
-
1236
- expect(text.plainText).toBe("Red Green Blue")
1237
-
1238
- await currentMouse.drag(text.x + 4, text.y, text.x + 9, text.y)
1239
- await renderOnce()
1240
-
1241
- expect(text.getSelectedText()).toBe("Green")
1242
- })
1243
-
1244
- it("should handle StyledText with TextNodeRenderable children", async () => {
1245
- const { text } = await createTextRenderable(currentRenderer, {
1246
- content: "",
1247
- selectable: true,
1248
- })
1249
-
1250
- const baseNode = new TextNodeRenderable({})
1251
- baseNode.add("Base ")
1252
- text.add(baseNode)
1253
-
1254
- const styledNode = new TextNodeRenderable({
1255
- fg: RGBA.fromValues(1, 0, 0, 1),
1256
- })
1257
-
1258
- const nodeStyledText = new StyledText([
1259
- { __isChunk: true, text: "Styled", fg: RGBA.fromValues(0, 1, 0, 1), attributes: 1 },
1260
- ])
1261
-
1262
- styledNode.add(nodeStyledText)
1263
- text.add(styledNode)
1264
-
1265
- await renderOnce()
1266
-
1267
- expect(text.plainText).toBe("Base Styled")
1268
-
1269
- await currentMouse.drag(text.x + 5, text.y, text.x + 11, text.y)
1270
- await renderOnce()
1271
- expect(text.getSelectedText()).toBe("Styled")
1272
- })
1273
- })
1274
-
1275
- describe("Text Selection with Truncation", () => {
1276
- it("should not extend selection across ellipsis in single line", async () => {
1277
- const buffer = currentRenderer.currentRenderBuffer
1278
- const { text } = await createTextRenderable(currentRenderer, {
1279
- content: "0123456789ABCDEFGHIJ",
1280
- width: 10,
1281
- height: 1,
1282
- selectable: true,
1283
- selectionBg: RGBA.fromValues(1, 0, 0, 1),
1284
- truncate: true,
1285
- })
1286
-
1287
- await currentMouse.drag(text.x + 6, text.y, text.x + 3, text.y)
1288
- await renderOnce()
1289
-
1290
- expect(text.hasSelection()).toBe(true)
1291
-
1292
- const { bg } = buffer.buffers
1293
- const bufferWidth = buffer.width
1294
-
1295
- const ellipsisIdx = text.y * bufferWidth + text.x + 3
1296
- const ellipsisBgR = bg[ellipsisIdx * 4 + 0]
1297
- const ellipsisBgG = bg[ellipsisIdx * 4 + 1]
1298
- const ellipsisBgB = bg[ellipsisIdx * 4 + 2]
1299
-
1300
- expect(Math.abs(ellipsisBgR - 1.0)).toBeLessThan(0.05)
1301
- expect(Math.abs(ellipsisBgG - 0.0)).toBeLessThan(0.05)
1302
- expect(Math.abs(ellipsisBgB - 0.0)).toBeLessThan(0.05)
1303
- })
1304
-
1305
- it("should render selection end correctly across ellipsis in last line", async () => {
1306
- const buffer = currentRenderer.currentRenderBuffer
1307
- const { text } = await createTextRenderable(currentRenderer, {
1308
- content: "Line 1: This is a long line without wrapping\nLine 2: Another very long line that will be truncated",
1309
- width: 10,
1310
- height: 2,
1311
- selectable: true,
1312
- selectionBg: RGBA.fromValues(1, 0, 0, 1),
1313
- truncate: true,
1314
- wrapMode: "none",
1315
- })
1316
-
1317
- await currentMouse.drag(text.x + 6, text.y, text.x + 2, text.y + 1)
1318
- await renderOnce()
1319
-
1320
- expect(text.hasSelection()).toBe(true)
1321
-
1322
- const { bg } = buffer.buffers
1323
- const bufferWidth = buffer.width
1324
-
1325
- const ellipsisIdx = (text.y + 1) * bufferWidth + text.x + 3
1326
- const ellipsisBgR = bg[ellipsisIdx * 4 + 0]
1327
- const ellipsisBgG = bg[ellipsisIdx * 4 + 1]
1328
- const ellipsisBgB = bg[ellipsisIdx * 4 + 2]
1329
-
1330
- expect(Math.abs(ellipsisBgR - 1.0)).toBeGreaterThan(0.05)
1331
- expect(Math.abs(ellipsisBgG - 0.0)).toBeLessThan(0.05)
1332
- expect(Math.abs(ellipsisBgB - 0.0)).toBeLessThan(0.05)
1333
- })
1334
- })
1335
-
1336
- describe("Text Content Snapshots", () => {
1337
- it("should render basic text content correctly", async () => {
1338
- await createTextRenderable(currentRenderer, {
1339
- content: "Hello World",
1340
- left: 5,
1341
- top: 3,
1342
- })
1343
-
1344
- const frame = captureFrame()
1345
- expect(frame).toMatchSnapshot()
1346
- })
1347
-
1348
- it("should render multiline text content correctly", async () => {
1349
- await createTextRenderable(currentRenderer, {
1350
- content: "Line 1: Hello\nLine 2: World\nLine 3: Testing\nLine 4: Multiline",
1351
- left: 1,
1352
- top: 1,
1353
- })
1354
-
1355
- const frame = captureFrame()
1356
- expect(frame).toMatchSnapshot()
1357
- })
1358
-
1359
- it("should render text with graphemes/emojis correctly", async () => {
1360
- await createTextRenderable(currentRenderer, {
1361
- content: "Hello 🌍 World 👋\n Test 🚀 Emoji",
1362
- left: 0,
1363
- top: 2,
1364
- })
1365
-
1366
- const frame = captureFrame()
1367
- expect(frame).toMatchSnapshot()
1368
- })
1369
-
1370
- it("should render TextNode text composition correctly", async () => {
1371
- const { text } = await createTextRenderable(currentRenderer, {
1372
- content: "",
1373
- left: 0,
1374
- top: 0,
1375
- })
1376
-
1377
- const node1 = new TextNodeRenderable({})
1378
- node1.add("First")
1379
-
1380
- const node2 = new TextNodeRenderable({})
1381
- node2.add(" Second")
1382
-
1383
- const node3 = new TextNodeRenderable({})
1384
- node3.add(" Third")
1385
-
1386
- text.add(node1)
1387
- text.add(node2)
1388
- text.add(node3)
1389
-
1390
- await renderOnce()
1391
-
1392
- const frame = captureFrame()
1393
- expect(frame).toMatchSnapshot()
1394
- })
1395
-
1396
- it("should render text positioning correctly", async () => {
1397
- await createTextRenderable(currentRenderer, {
1398
- content: "Top",
1399
- position: "absolute",
1400
- left: 0,
1401
- top: 0,
1402
- })
1403
-
1404
- await createTextRenderable(currentRenderer, {
1405
- content: "Mid",
1406
- position: "absolute",
1407
- left: 8,
1408
- top: 2,
1409
- })
1410
-
1411
- await createTextRenderable(currentRenderer, {
1412
- content: "Bot",
1413
- position: "absolute",
1414
- left: 16,
1415
- top: 4,
1416
- })
1417
-
1418
- const frame = captureFrame()
1419
- expect(frame).toMatchSnapshot()
1420
- })
1421
-
1422
- it("should render empty buffer correctly", async () => {
1423
- currentRenderer.currentRenderBuffer.clear()
1424
- const frame = captureFrame()
1425
- expect(frame).toMatchSnapshot()
1426
- })
1427
-
1428
- it("should render text with character wrapping correctly", async () => {
1429
- const { text } = await createTextRenderable(currentRenderer, {
1430
- content: "This is a very long text that should wrap to multiple lines when wrap is enabled",
1431
- wrapMode: "char", // Explicitly test character wrapping
1432
- width: 15, // Force wrapping at 15 characters width
1433
- left: 0,
1434
- top: 0,
1435
- })
1436
-
1437
- const frame = captureFrame()
1438
- expect(frame).toMatchSnapshot()
1439
- })
1440
-
1441
- it("should render wrapped text with different content", async () => {
1442
- await createTextRenderable(currentRenderer, {
1443
- content: "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789",
1444
- wrapMode: "char", // Explicitly test character wrapping
1445
- width: 10, // Force wrapping at 10 characters width
1446
- left: 2,
1447
- top: 1,
1448
- })
1449
-
1450
- const frame = captureFrame()
1451
- expect(frame).toMatchSnapshot()
1452
- })
1453
-
1454
- it("should render wrapped text with emojis and graphemes", async () => {
1455
- await createTextRenderable(currentRenderer, {
1456
- content: "Hello 🌍 World 👋 This is a test with emojis 🚀 that should wrap properly",
1457
- wrapMode: "char", // Explicitly test character wrapping
1458
- width: 12, // Force wrapping at 12 characters width
1459
- left: 1,
1460
- top: 0,
1461
- })
1462
-
1463
- const frame = captureFrame()
1464
- expect(frame).toMatchSnapshot()
1465
- })
1466
-
1467
- it("should render wrapped multiline text correctly", async () => {
1468
- await createTextRenderable(currentRenderer, {
1469
- content: "First line with long content\nSecond line also with content\nThird line",
1470
- wrapMode: "char", // Explicitly test character wrapping
1471
- width: 8, // Force wrapping at 8 characters width
1472
- left: 0,
1473
- top: 1,
1474
- })
1475
-
1476
- const frame = captureFrame()
1477
- expect(frame).toMatchSnapshot()
1478
- })
1479
-
1480
- it("should render text with tab indicator correctly", async () => {
1481
- await createTextRenderable(currentRenderer, {
1482
- content: "Line 1\tTabbed\nLine 2\t\tDouble tab",
1483
- tabIndicator: "→",
1484
- tabIndicatorColor: RGBA.fromValues(0.5, 0.5, 0.5, 1),
1485
- left: 0,
1486
- top: 0,
1487
- })
1488
-
1489
- const frame = captureFrame()
1490
- expect(frame).toMatchSnapshot()
1491
- })
1492
-
1493
- it("should render word wrapped text with CJK and English correctly", async () => {
1494
- resize(60, 10)
1495
-
1496
- const { text } = await createTextRenderable(currentRenderer, {
1497
- content: "🌟 Unicode test: こんにちは世界 Hello World 你好世界",
1498
- wrapMode: "word",
1499
- width: 35,
1500
- left: 0,
1501
- top: 0,
1502
- })
1503
-
1504
- await renderOnce()
1505
-
1506
- const frame = captureFrame()
1507
- const lines = frame.split("\n").filter((l) => l.trim().length > 0)
1508
-
1509
- // Verify no character duplication - each character should appear only once
1510
- const line0 = lines[0] || ""
1511
- const line1 = lines[1] || ""
1512
-
1513
- const line0_ends_with_kai = line0.trimEnd().endsWith("界")
1514
- const line1_starts_with_kai = line1.trimStart().startsWith("界")
1515
-
1516
- // "界" should not appear on both lines (would indicate duplication bug)
1517
- expect(line0_ends_with_kai && line1_starts_with_kai).toBe(false)
1518
- })
1519
-
1520
- it("should not split English word 'Hello' in middle when word wrapping with CJK characters", async () => {
1521
- // This test reproduces the exact issue from text-truncation-demo.ts where "Hello"
1522
- // is incorrectly split as "Hell" on first line and "o World" on second line
1523
- // when word wrapping is enabled with CJK/emoji characters before it.
1524
- resize(60, 10)
1525
-
1526
- const { text } = await createTextRenderable(currentRenderer, {
1527
- content: "🌟 Unicode test: こんにちは世界 Hello World 你好世界 안녕하세요 🚀 More emoji: 🎨🎭🎪🎬🎮🎯",
1528
- wrapMode: "word",
1529
- width: 50, // Width that causes wrapping in the demo
1530
- left: 0,
1531
- top: 0,
1532
- })
1533
-
1534
- await renderOnce()
1535
-
1536
- const frame = captureFrame()
1537
-
1538
- const lines = frame.split("\n").filter((l) => l.trim().length > 0)
1539
-
1540
- // The word "Hello" should NOT be split in the middle
1541
- // Check for the specific incorrect split: "Hell" on one line, "o" starting the next
1542
- let foundIncorrectSplit = false
1543
- for (let i = 0; i < lines.length - 1; i++) {
1544
- const currentLine = lines[i] || ""
1545
- const nextLine = lines[i + 1] || ""
1546
-
1547
- // Check if current line ends with "Hell" (incorrect split)
1548
- if (currentLine.trimEnd().endsWith("Hell")) {
1549
- // And next line starts with "o" (the rest of "Hello")
1550
- if (nextLine.trimStart().startsWith("o")) {
1551
- foundIncorrectSplit = true
1552
- }
1553
- }
1554
- }
1555
-
1556
- // Verify "Hello" is not split as "Hell" + "o"
1557
- expect(foundIncorrectSplit).toBe(false)
1558
-
1559
- // Verify the word "Hello" appears complete on a single line
1560
- const fullText = lines.join(" ")
1561
- expect(fullText).toContain("Hello")
1562
-
1563
- // Verify "Hello" is not split in the middle
1564
- const helloLineIndex = lines.findIndex((line) => line.includes("Hello"))
1565
- expect(helloLineIndex).toBeGreaterThanOrEqual(0) // "Hello" should be found
1566
-
1567
- const helloLine = lines[helloLineIndex] || ""
1568
- // Verify "Hello" appears as a complete word on this line
1569
- expect(helloLine).toMatch(/Hello/)
1570
-
1571
- // Verify no previous line ends with "Hell" without "o"
1572
- if (helloLineIndex > 0) {
1573
- const prevLine = lines[helloLineIndex - 1] || ""
1574
- expect(prevLine.trimEnd().endsWith("Hell")).toBe(false)
1575
- }
1576
-
1577
- // Additional verification: "Hello World" should ideally be together
1578
- // (this is a nice-to-have, showing improved wrapping behavior)
1579
- expect(helloLine).toContain("Hello World")
1580
- })
1581
- })
1582
-
1583
- describe("Text Node Dimension Updates", () => {
1584
- it("should update dimensions and reposition subsequent elements when text nodes expand", async () => {
1585
- const { text: firstText } = await createTextRenderable(currentRenderer, {
1586
- content: "",
1587
- width: 20,
1588
- wrapMode: "char",
1589
- })
1590
-
1591
- const shortNode = new TextNodeRenderable({})
1592
- shortNode.add("Short")
1593
- firstText.add(shortNode)
1594
-
1595
- const { text: secondText } = await createTextRenderable(currentRenderer, {
1596
- content: "Second text",
1597
- })
1598
-
1599
- await renderOnce()
1600
- const initialFrame = captureFrame()
1601
- expect(initialFrame).toMatchSnapshot()
1602
-
1603
- expect(firstText.height).toEqual(1)
1604
- expect(secondText.y).toEqual(1)
1605
-
1606
- shortNode.add(" text that will definitely wrap")
1607
-
1608
- await renderOnce()
1609
-
1610
- const finalFrame = captureFrame()
1611
-
1612
- expect(firstText.height).toEqual(2)
1613
- expect(secondText.y).toEqual(2)
1614
-
1615
- expect(finalFrame).not.toBe(initialFrame)
1616
- expect(finalFrame).toMatchSnapshot()
1617
- })
1618
-
1619
- it("should handle multiple text node updates with complex layout changes", async () => {
1620
- resize(20, 10)
1621
- const { text: firstText } = await createTextRenderable(currentRenderer, {
1622
- width: 10,
1623
- wrapMode: "word",
1624
- })
1625
-
1626
- const node1 = TextNodeRenderable.fromString("First")
1627
- const node2 = TextNodeRenderable.fromString(" part")
1628
-
1629
- firstText.add(node1)
1630
- firstText.add(node2)
1631
-
1632
- const { text: secondText } = await createTextRenderable(currentRenderer, {
1633
- width: 12,
1634
- wrapMode: "word",
1635
- })
1636
- secondText.add("Middle text")
1637
-
1638
- const { text: thirdText } = await createTextRenderable(currentRenderer, {})
1639
- thirdText.add("Bottom text")
1640
-
1641
- await renderOnce()
1642
- const initialFrame = captureFrame()
1643
- expect(initialFrame).toMatchSnapshot()
1644
-
1645
- // Record initial positions
1646
- expect(firstText.height).toEqual(1)
1647
- expect(secondText.y).toEqual(1)
1648
- expect(thirdText.y).toEqual(2)
1649
-
1650
- node1.add(" of a sentence")
1651
- node2.add("that will wrap")
1652
-
1653
- await renderOnce()
1654
-
1655
- const finalFrame = captureFrame()
1656
- expect(finalFrame).toMatchSnapshot()
1657
-
1658
- expect(firstText.height).toEqual(5)
1659
- expect(secondText.y).toEqual(5)
1660
- expect(thirdText.y).toEqual(6)
1661
- })
1662
- })
1663
-
1664
- describe("Height and Width Measurement", () => {
1665
- it("should grow height for multiline text without wrapping", async () => {
1666
- const { text } = await createTextRenderable(currentRenderer, {
1667
- content: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
1668
- wrapMode: "none",
1669
- })
1670
-
1671
- await renderOnce()
1672
-
1673
- expect(text.height).toBe(5)
1674
- expect(text.width).toBeGreaterThanOrEqual(6)
1675
- })
1676
-
1677
- it("should grow height for wrapped text when wrapping enabled", async () => {
1678
- const { text } = await createTextRenderable(currentRenderer, {
1679
- content: "This is a very long line that will definitely wrap to multiple lines",
1680
- wrapMode: "word",
1681
- width: 15,
1682
- })
1683
-
1684
- await renderOnce()
1685
-
1686
- expect(text.height).toBeGreaterThan(1)
1687
- expect(text.width).toBeLessThanOrEqual(15)
1688
- })
1689
-
1690
- it("should measure full width when wrapping is disabled and not constrained by parent", async () => {
1691
- const longLine = "This is a very long line that would wrap but wrapping is disabled"
1692
- const { text } = await createTextRenderable(currentRenderer, {
1693
- content: longLine,
1694
- wrapMode: "none",
1695
- position: "absolute",
1696
- })
1697
-
1698
- await renderOnce()
1699
-
1700
- expect(text.height).toBe(1)
1701
- expect(text.width).toBe(longLine.length)
1702
- })
1703
-
1704
- it("should update height when content changes from single to multiline", async () => {
1705
- const { text } = await createTextRenderable(currentRenderer, {
1706
- content: "Single line",
1707
- wrapMode: "none",
1708
- })
1709
-
1710
- await renderOnce()
1711
- expect(text.height).toBe(1)
1712
-
1713
- text.content = "Line 1\nLine 2\nLine 3"
1714
- await renderOnce()
1715
-
1716
- expect(text.height).toBe(3)
1717
- })
1718
-
1719
- it("should update height when wrapping mode changes", async () => {
1720
- const { text } = await createTextRenderable(currentRenderer, {
1721
- content: "This is a long line that will wrap to multiple lines",
1722
- wrapMode: "none",
1723
- width: 15,
1724
- })
1725
-
1726
- await renderOnce()
1727
- const unwrappedHeight = text.height
1728
- expect(unwrappedHeight).toBe(1)
1729
- expect(text.width).toBe(15)
1730
-
1731
- text.wrapMode = "word"
1732
- await renderOnce()
1733
-
1734
- const wrappedHeight = text.height
1735
-
1736
- expect(wrappedHeight).toBeGreaterThan(unwrappedHeight)
1737
- expect(wrappedHeight).toBeGreaterThanOrEqual(3)
1738
- })
1739
-
1740
- it("should shrink height when content changes from multi-line to single line", async () => {
1741
- const { text } = await createTextRenderable(currentRenderer, {
1742
- content: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
1743
- wrapMode: "none",
1744
- })
1745
-
1746
- await renderOnce()
1747
- expect(text.height).toBe(5)
1748
-
1749
- text.content = "Single line"
1750
- await renderOnce()
1751
-
1752
- expect(text.height).toBe(1)
1753
- })
1754
-
1755
- it("should shrink width when replacing long line with shorter (wrapMode: none, position: absolute)", async () => {
1756
- const { text } = await createTextRenderable(currentRenderer, {
1757
- content: "This is a very long line with many characters",
1758
- wrapMode: "none",
1759
- position: "absolute",
1760
- })
1761
-
1762
- await renderOnce()
1763
- const initialWidth = text.width
1764
- expect(initialWidth).toBe(45) // length of the long line
1765
-
1766
- text.content = "Short"
1767
- await renderOnce()
1768
-
1769
- expect(text.width).toBe(5)
1770
- expect(text.width).toBeLessThan(initialWidth)
1771
- })
1772
- })
1773
-
1774
- describe("Width/Height Setter Layout Tests", () => {
1775
- it("should not shrink box when width is set via setter", async () => {
1776
- resize(40, 10)
1777
-
1778
- const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })
1779
- currentRenderer.root.add(container)
1780
-
1781
- const row = new BoxRenderable(currentRenderer, { flexDirection: "row", width: "100%" })
1782
- container.add(row)
1783
-
1784
- const indicator = new BoxRenderable(currentRenderer, { backgroundColor: "#f00" })
1785
- row.add(indicator)
1786
-
1787
- const indicatorText = new TextRenderable(currentRenderer, { content: ">" })
1788
- indicator.add(indicatorText)
1789
-
1790
- const content = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1791
- row.add(content)
1792
-
1793
- const contentText = new TextRenderable(currentRenderer, { content: "Content that takes up space" })
1794
- content.add(contentText)
1795
-
1796
- await renderOnce()
1797
-
1798
- const initialIndicatorWidth = indicator.width
1799
-
1800
- indicator.width = 5
1801
- await renderOnce()
1802
-
1803
- const frame = captureFrame()
1804
- expect(frame).toMatchSnapshot()
1805
-
1806
- expect(indicator.width).toBe(5)
1807
- expect(content.width).toBeGreaterThan(0)
1808
- expect(content.width).toBeLessThan(30) // Should be compressed but not zero
1809
- })
1810
-
1811
- it("should not shrink box when height is set via setter in column layout with text", async () => {
1812
- resize(30, 15)
1813
-
1814
- const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })
1815
- currentRenderer.root.add(outerBox)
1816
-
1817
- const column = new BoxRenderable(currentRenderer, { flexDirection: "column", height: "100%" })
1818
- outerBox.add(column)
1819
-
1820
- const header = new BoxRenderable(currentRenderer, { backgroundColor: "#f00" })
1821
- column.add(header)
1822
-
1823
- const headerText = new TextRenderable(currentRenderer, { content: "Header" })
1824
- header.add(headerText)
1825
-
1826
- const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1827
- column.add(mainContent)
1828
-
1829
- const mainText = new TextRenderable(currentRenderer, {
1830
- content: "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8",
1831
- })
1832
- mainContent.add(mainText)
1833
-
1834
- const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: "#00f" })
1835
- column.add(footer)
1836
-
1837
- const footerText = new TextRenderable(currentRenderer, { content: "Footer" })
1838
- footer.add(footerText)
1839
-
1840
- await renderOnce()
1841
-
1842
- header.height = 3
1843
- await renderOnce()
1844
-
1845
- const frame = captureFrame()
1846
- expect(frame).toMatchSnapshot()
1847
-
1848
- expect(header.height).toBe(3)
1849
- expect(mainContent.height).toBeGreaterThan(0)
1850
- expect(footer.height).toBe(2)
1851
- })
1852
-
1853
- it("should not shrink box when minWidth is set via setter", async () => {
1854
- resize(40, 10)
1855
-
1856
- const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })
1857
- currentRenderer.root.add(container)
1858
-
1859
- const row = new BoxRenderable(currentRenderer, { flexDirection: "row", width: "100%" })
1860
- container.add(row)
1861
-
1862
- const indicator = new BoxRenderable(currentRenderer, { backgroundColor: "#f00", flexShrink: 1 })
1863
- row.add(indicator)
1864
-
1865
- const indicatorText = new TextRenderable(currentRenderer, { content: ">" })
1866
- indicator.add(indicatorText)
1867
-
1868
- const content = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1869
- row.add(content)
1870
-
1871
- const contentText = new TextRenderable(currentRenderer, { content: "Content that takes up space" })
1872
- content.add(contentText)
1873
-
1874
- await renderOnce()
1875
-
1876
- indicator.minWidth = 5
1877
- await renderOnce()
1878
-
1879
- const frame = captureFrame()
1880
- expect(frame).toMatchSnapshot()
1881
-
1882
- expect(indicator.width).toBeGreaterThanOrEqual(5)
1883
- expect(content.width).toBeGreaterThan(0)
1884
- })
1885
-
1886
- it("should not shrink box when minHeight is set via setter in column layout with text", async () => {
1887
- resize(30, 15)
1888
-
1889
- const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })
1890
- currentRenderer.root.add(outerBox)
1891
-
1892
- const column = new BoxRenderable(currentRenderer, { flexDirection: "column", height: "100%" })
1893
- outerBox.add(column)
1894
-
1895
- const header = new BoxRenderable(currentRenderer, { backgroundColor: "#f00", flexShrink: 1 })
1896
- column.add(header)
1897
-
1898
- const headerText = new TextRenderable(currentRenderer, { content: "Header" })
1899
- header.add(headerText)
1900
-
1901
- const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1902
- column.add(mainContent)
1903
-
1904
- const mainText = new TextRenderable(currentRenderer, {
1905
- content: "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8",
1906
- })
1907
- mainContent.add(mainText)
1908
-
1909
- const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: "#00f" })
1910
- column.add(footer)
1911
-
1912
- const footerText = new TextRenderable(currentRenderer, { content: "Footer" })
1913
- footer.add(footerText)
1914
-
1915
- await renderOnce()
1916
-
1917
- header.minHeight = 3
1918
- await renderOnce()
1919
-
1920
- const frame = captureFrame()
1921
- expect(frame).toMatchSnapshot()
1922
-
1923
- expect(header.height).toBeGreaterThanOrEqual(3)
1924
- expect(mainContent.height).toBeGreaterThan(0)
1925
- expect(footer.height).toBe(2)
1926
- })
1927
-
1928
- it("should not shrink box when width is set from undefined via setter", async () => {
1929
- resize(40, 10)
1930
-
1931
- const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })
1932
- currentRenderer.root.add(container)
1933
-
1934
- const row = new BoxRenderable(currentRenderer, { flexDirection: "row", width: "100%" })
1935
- container.add(row)
1936
-
1937
- const indicator = new BoxRenderable(currentRenderer, { backgroundColor: "#f00", flexShrink: 1 })
1938
- row.add(indicator)
1939
-
1940
- const indicatorText = new TextRenderable(currentRenderer, { content: ">" })
1941
- indicator.add(indicatorText)
1942
-
1943
- const content = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1944
- row.add(content)
1945
-
1946
- const contentText = new TextRenderable(currentRenderer, { content: "Content that takes up space" })
1947
- content.add(contentText)
1948
-
1949
- await renderOnce()
1950
-
1951
- indicator.width = 5
1952
- await renderOnce()
1953
-
1954
- const frame = captureFrame()
1955
- expect(frame).toMatchSnapshot()
1956
-
1957
- expect(indicator.width).toBe(5)
1958
- expect(content.width).toBeGreaterThan(0)
1959
- })
1960
- })
1961
-
1962
- describe("Absolute Positioned Box with Text", () => {
1963
- it("should render text in absolute positioned box with padding and borders correctly", async () => {
1964
- resize(80, 20)
1965
-
1966
- const notificationBox = new BoxRenderable(currentRenderer, {
1967
- position: "absolute",
1968
- justifyContent: "center",
1969
- alignItems: "flex-start",
1970
- top: 2,
1971
- right: 2,
1972
- width: Math.min(60, 80 - 6),
1973
- paddingLeft: 2,
1974
- paddingRight: 2,
1975
- paddingTop: 1,
1976
- paddingBottom: 1,
1977
- backgroundColor: "#1e293b",
1978
- borderColor: "#3b82f6",
1979
- border: ["left", "right"],
1980
- })
1981
-
1982
- currentRenderer.root.add(notificationBox)
1983
-
1984
- // Wrap content in nested boxes with row layout and gap
1985
- const outerWrapperBox = new BoxRenderable(currentRenderer, {
1986
- flexDirection: "row",
1987
- paddingBottom: 1,
1988
- paddingTop: 1,
1989
- paddingLeft: 2,
1990
- paddingRight: 2,
1991
- gap: 2,
1992
- })
1993
- notificationBox.add(outerWrapperBox)
1994
-
1995
- const innerContentBox = new BoxRenderable(currentRenderer, {
1996
- flexGrow: 1,
1997
- gap: 1,
1998
- })
1999
- outerWrapperBox.add(innerContentBox)
2000
-
2001
- const titleText = new TextRenderable(currentRenderer, {
2002
- content: "Important Notification",
2003
- attributes: 1, // BOLD
2004
- marginBottom: 1,
2005
- fg: "#f8fafc",
2006
- })
2007
- innerContentBox.add(titleText)
2008
-
2009
- const messageText = new TextRenderable(currentRenderer, {
2010
- content:
2011
- "This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.",
2012
- fg: "#e2e8f0",
2013
- wrapMode: "word",
2014
- width: "100%",
2015
- })
2016
- innerContentBox.add(messageText)
2017
-
2018
- await renderOnce()
2019
-
2020
- const frame = captureFrame()
2021
- expect(frame).toMatchSnapshot()
2022
-
2023
- // Verify the box is positioned correctly
2024
- expect(notificationBox.x).toBeGreaterThan(0)
2025
- expect(notificationBox.y).toBe(2)
2026
- expect(notificationBox.width).toBe(60)
2027
-
2028
- // Note: With current Yoga behavior, nested flex boxes with width:"100%" inside
2029
- // an absolutely positioned parent with only maxWidth (no explicit width) causes
2030
- // the children to grow to their intrinsic size rather than being constrained
2031
- // This is Yoga's shrink-to-fit behavior with the circular dependency
2032
- // See: https://github.com/facebook/yoga/issues/1409
2033
- expect(outerWrapperBox.width).toBeGreaterThan(100)
2034
- expect(innerContentBox.width).toBeGreaterThan(100)
2035
- expect(messageText.width).toBeGreaterThan(100)
2036
- expect(messageText.height).toBe(1)
2037
- expect(messageText.plainText).toBe(
2038
- "This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.",
2039
- )
2040
- })
2041
-
2042
- it("should render text fully visible in absolute positioned box at various positions", async () => {
2043
- resize(100, 25)
2044
-
2045
- // Top-right positioned box
2046
- const topRightBox = new BoxRenderable(currentRenderer, {
2047
- position: "absolute",
2048
- top: 1,
2049
- right: 1,
2050
- maxWidth: 40,
2051
- paddingLeft: 1,
2052
- paddingRight: 1,
2053
- paddingTop: 0,
2054
- paddingBottom: 0,
2055
- backgroundColor: "#fef2f2",
2056
- borderColor: "#ef4444",
2057
- border: true,
2058
- })
2059
- currentRenderer.root.add(topRightBox)
2060
-
2061
- const topRightText = new TextRenderable(currentRenderer, {
2062
- content: "Error: File not found in the specified directory path",
2063
- fg: "#991b1b",
2064
- wrapMode: "word",
2065
- width: "100%",
2066
- })
2067
- topRightBox.add(topRightText)
2068
-
2069
- // Bottom-left positioned box
2070
- const bottomLeftBox = new BoxRenderable(currentRenderer, {
2071
- position: "absolute",
2072
- bottom: 1,
2073
- left: 1,
2074
- maxWidth: 35,
2075
- paddingLeft: 1,
2076
- paddingRight: 1,
2077
- backgroundColor: "#f0fdf4",
2078
- borderColor: "#22c55e",
2079
- border: ["top", "bottom"],
2080
- })
2081
- currentRenderer.root.add(bottomLeftBox)
2082
-
2083
- const bottomLeftText = new TextRenderable(currentRenderer, {
2084
- content: "Success: Operation completed successfully!",
2085
- fg: "#166534",
2086
- wrapMode: "word",
2087
- width: "100%",
2088
- })
2089
- bottomLeftBox.add(bottomLeftText)
2090
-
2091
- await renderOnce()
2092
-
2093
- const frame = captureFrame()
2094
- expect(frame).toMatchSnapshot()
2095
-
2096
- // Verify top-right box positioning and dimensions
2097
- expect(topRightBox.y).toBe(1)
2098
- expect(topRightBox.x).toBeGreaterThan(50)
2099
- expect(topRightBox.width).toBeGreaterThan(30)
2100
- expect(topRightBox.width).toBeLessThanOrEqual(40)
2101
-
2102
- // Verify top-right text renders with proper width
2103
- expect(topRightText.plainText).toBe("Error: File not found in the specified directory path")
2104
- expect(topRightText.width).toBeGreaterThan(25)
2105
- expect(topRightText.width).toBeLessThanOrEqual(38)
2106
- expect(topRightText.height).toBeGreaterThan(1)
2107
-
2108
- // Verify bottom-left box positioning and dimensions
2109
- expect(bottomLeftBox.x).toBe(1)
2110
- expect(bottomLeftBox.y).toBeGreaterThan(15)
2111
- expect(bottomLeftBox.width).toBeGreaterThan(25)
2112
- expect(bottomLeftBox.width).toBeLessThanOrEqual(35)
2113
-
2114
- // Verify bottom-left text renders with proper width
2115
- expect(bottomLeftText.plainText).toBe("Success: Operation completed successfully!")
2116
- expect(bottomLeftText.width).toBeGreaterThan(25)
2117
- expect(bottomLeftText.width).toBeLessThanOrEqual(33)
2118
- expect(bottomLeftText.height).toBeGreaterThan(1)
2119
- expect(bottomLeftText.width).toBeGreaterThan(0)
2120
- expect(bottomLeftText.width).toBeLessThanOrEqual(33) // maxWidth 35 - padding 2
2121
- })
2122
-
2123
- it("should handle width:100% text in absolute positioned box with constrained maxWidth", async () => {
2124
- resize(70, 15)
2125
-
2126
- const constrainedBox = new BoxRenderable(currentRenderer, {
2127
- position: "absolute",
2128
- top: 5,
2129
- left: 10,
2130
- maxWidth: 50,
2131
- paddingLeft: 3,
2132
- paddingRight: 3,
2133
- paddingTop: 2,
2134
- paddingBottom: 2,
2135
- backgroundColor: "#1e1e2e",
2136
- })
2137
- currentRenderer.root.add(constrainedBox)
2138
-
2139
- const longText = new TextRenderable(currentRenderer, {
2140
- content:
2141
- "This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.",
2142
- fg: "#cdd6f4",
2143
- wrapMode: "word",
2144
- width: "100%",
2145
- })
2146
- constrainedBox.add(longText)
2147
-
2148
- await renderOnce()
2149
-
2150
- const frame = captureFrame()
2151
- expect(frame).toMatchSnapshot()
2152
-
2153
- // Verify the box respects maxWidth
2154
- expect(constrainedBox.width).toBeLessThanOrEqual(50)
2155
- expect(constrainedBox.width).toBeGreaterThan(40)
2156
- expect(constrainedBox.x).toBe(10)
2157
- expect(constrainedBox.y).toBe(5)
2158
-
2159
- // Verify text wraps and fills available width
2160
- expect(longText.width).toBeGreaterThan(35)
2161
- expect(longText.width).toBeLessThanOrEqual(44)
2162
- expect(longText.height).toBeGreaterThanOrEqual(5)
2163
- expect(longText.plainText).toBe(
2164
- "This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.",
2165
- )
2166
- })
2167
-
2168
- it("should render multiple text elements in absolute positioned box with proper spacing", async () => {
2169
- resize(90, 20)
2170
-
2171
- const infoBox = new BoxRenderable(currentRenderer, {
2172
- position: "absolute",
2173
- justifyContent: "flex-start",
2174
- alignItems: "flex-start",
2175
- top: 3,
2176
- right: 5,
2177
- maxWidth: 45,
2178
- paddingLeft: 2,
2179
- paddingRight: 2,
2180
- paddingTop: 1,
2181
- paddingBottom: 1,
2182
- backgroundColor: "#eff6ff",
2183
- borderColor: "#3b82f6",
2184
- border: true,
2185
- })
2186
- currentRenderer.root.add(infoBox)
2187
-
2188
- const headerText = new TextRenderable(currentRenderer, {
2189
- content: "System Update",
2190
- attributes: 1, // BOLD
2191
- fg: "#1e40af",
2192
- })
2193
- infoBox.add(headerText)
2194
-
2195
- const bodyText = new TextRenderable(currentRenderer, {
2196
- content: "A new version is available with bug fixes and performance improvements.",
2197
- fg: "#1e3a8a",
2198
- wrapMode: "word",
2199
- width: "100%",
2200
- marginTop: 1,
2201
- })
2202
- infoBox.add(bodyText)
2203
-
2204
- const footerText = new TextRenderable(currentRenderer, {
2205
- content: "Click to install",
2206
- fg: "#60a5fa",
2207
- marginTop: 1,
2208
- })
2209
- infoBox.add(footerText)
2210
-
2211
- await renderOnce()
2212
-
2213
- const frame = captureFrame()
2214
- expect(frame).toMatchSnapshot()
2215
-
2216
- // Verify all texts are rendered with correct content
2217
- expect(headerText.plainText).toBe("System Update")
2218
- expect(bodyText.plainText).toBe("A new version is available with bug fixes and performance improvements.")
2219
- expect(footerText.plainText).toBe("Click to install")
2220
-
2221
- // Verify box dimensions are reasonable
2222
- expect(infoBox.width).toBeGreaterThan(35)
2223
- expect(infoBox.width).toBeLessThanOrEqual(45)
2224
-
2225
- // Verify header text renders properly
2226
- expect(headerText.width).toBeGreaterThan(10)
2227
- expect(headerText.height).toBe(1)
2228
-
2229
- // Verify body text fills width and wraps
2230
- expect(bodyText.width).toBeGreaterThan(30)
2231
- expect(bodyText.height).toBeGreaterThanOrEqual(2)
2232
-
2233
- // Verify footer text renders properly
2234
- expect(footerText.width).toBeGreaterThan(10)
2235
- expect(footerText.height).toBe(1)
2236
-
2237
- // Verify vertical spacing
2238
- expect(bodyText.y).toBeGreaterThan(headerText.y)
2239
- expect(footerText.y).toBeGreaterThan(bodyText.y)
2240
- })
2241
- })
2242
-
2243
- describe("Word Wrapping", () => {
2244
- it("should default to word wrap mode", async () => {
2245
- const { text } = await createTextRenderable(currentRenderer, {
2246
- content: "Hello World",
2247
- })
2248
-
2249
- expect(text.wrapMode).toBe("word")
2250
- })
2251
-
2252
- it("should wrap at word boundaries when using word mode", async () => {
2253
- await createTextRenderable(currentRenderer, {
2254
- content: "The quick brown fox jumps over the lazy dog",
2255
- wrapMode: "word",
2256
- width: 15,
2257
- left: 0,
2258
- top: 0,
2259
- })
2260
-
2261
- const frame = captureFrame()
2262
- expect(frame).toMatchSnapshot()
2263
- })
2264
-
2265
- it("should wrap at character boundaries when using char mode", async () => {
2266
- const { text } = await createTextRenderable(currentRenderer, {
2267
- content: "The quick brown fox jumps over the lazy dog",
2268
- wrapMode: "char",
2269
- width: 15,
2270
- left: 0,
2271
- top: 0,
2272
- })
2273
-
2274
- const frame = captureFrame()
2275
- expect(frame).toMatchSnapshot()
2276
- })
2277
-
2278
- it("should handle word wrapping with punctuation", async () => {
2279
- await createTextRenderable(currentRenderer, {
2280
- content: "Hello,World.Test-Example/Path",
2281
- wrapMode: "word",
2282
- width: 10,
2283
- left: 0,
2284
- top: 0,
2285
- })
2286
-
2287
- const frame = captureFrame()
2288
- expect(frame).toMatchSnapshot()
2289
- })
2290
-
2291
- it("should handle word wrapping with hyphens and dashes", async () => {
2292
- await createTextRenderable(currentRenderer, {
2293
- content: "self-contained multi-line text-wrapping example",
2294
- wrapMode: "word",
2295
- width: 12,
2296
- left: 0,
2297
- top: 0,
2298
- })
2299
-
2300
- const frame = captureFrame()
2301
- expect(frame).toMatchSnapshot()
2302
- })
2303
-
2304
- it("regression #651: should keep multi-byte UTF-8 words intact when wrapping in word mode", async () => {
2305
- resize(80, 24)
2306
-
2307
- await createTextRenderable(currentRenderer, {
2308
- content: "gyorskiszolgáló éttermek közül. Azóta alapjaiban értelmeztük újra a vendéglátást",
2309
- wrapMode: "word",
2310
- width: 40,
2311
- left: 0,
2312
- top: 0,
2313
- })
2314
-
2315
- const lines = captureFrame()
2316
- .split("\n")
2317
- .map((line) => line.trimEnd())
2318
- .filter((line) => line.length > 0)
2319
-
2320
- const expectedLines = ["gyorskiszolgáló éttermek közül. Azóta", "alapjaiban értelmeztük újra a", "vendéglátást"]
2321
-
2322
- expect(lines).toEqual(expectedLines)
2323
- })
2324
-
2325
- it("should dynamically change wrap mode", async () => {
2326
- const { text } = await createTextRenderable(currentRenderer, {
2327
- content: "The quick brown fox jumps",
2328
- wrapMode: "char",
2329
- width: 10,
2330
- left: 0,
2331
- top: 0,
2332
- })
2333
-
2334
- expect(text.wrapMode).toBe("char")
2335
-
2336
- // Change to word mode
2337
- text.wrapMode = "word"
2338
- await renderOnce()
2339
-
2340
- expect(text.wrapMode).toBe("word")
2341
- const frame = captureFrame()
2342
- expect(frame).toMatchSnapshot()
2343
- })
2344
-
2345
- it("should handle long words that exceed wrap width in word mode", async () => {
2346
- await createTextRenderable(currentRenderer, {
2347
- content: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
2348
- wrapMode: "word",
2349
- width: 10,
2350
- left: 0,
2351
- top: 0,
2352
- })
2353
-
2354
- // Since there's no word boundary, it should fall back to character wrapping
2355
- const frame = captureFrame()
2356
- expect(frame).toMatchSnapshot()
2357
- })
2358
-
2359
- it("should preserve empty lines with word wrapping", async () => {
2360
- await createTextRenderable(currentRenderer, {
2361
- content: "First line\n\nThird line",
2362
- wrapMode: "word",
2363
- width: 8,
2364
- left: 0,
2365
- top: 0,
2366
- })
2367
-
2368
- const frame = captureFrame()
2369
- expect(frame).toMatchSnapshot()
2370
- })
2371
-
2372
- it("should handle word wrapping with single character words", async () => {
2373
- await createTextRenderable(currentRenderer, {
2374
- content: "a b c d e f g h i j k l m n o p",
2375
- wrapMode: "word",
2376
- width: 8,
2377
- left: 0,
2378
- top: 0,
2379
- })
2380
-
2381
- const frame = captureFrame()
2382
- expect(frame).toMatchSnapshot()
2383
- })
2384
-
2385
- it("should compare char vs word wrapping with same content", async () => {
2386
- const content = "Hello wonderful world of text wrapping"
2387
-
2388
- // Test with char mode
2389
- const { text: charText } = await createTextRenderable(currentRenderer, {
2390
- content,
2391
- wrapMode: "char",
2392
- width: 12,
2393
- left: 0,
2394
- top: 0,
2395
- })
2396
-
2397
- const charFrame = captureFrame()
2398
-
2399
- // Remove the char text and add word text
2400
- currentRenderer.root.remove(charText.id)
2401
- await renderOnce()
2402
-
2403
- await createTextRenderable(currentRenderer, {
2404
- content,
2405
- wrapMode: "word",
2406
- width: 12,
2407
- left: 0,
2408
- top: 0,
2409
- })
2410
-
2411
- const wordFrame = captureFrame()
2412
-
2413
- // The frames should be different as word wrapping preserves word boundaries
2414
- expect(charFrame).not.toBe(wordFrame)
2415
- expect(wordFrame).toMatchSnapshot()
2416
- })
2417
-
2418
- it("should correctly wrap text when updating content via text.content", async () => {
2419
- const { text } = await createTextRenderable(currentRenderer, {
2420
- content: "Short text",
2421
- wrapMode: "word",
2422
- left: 0,
2423
- top: 0,
2424
- })
2425
-
2426
- await renderOnce()
2427
- const initialFrame = captureFrame()
2428
- expect(initialFrame).toMatchSnapshot()
2429
-
2430
- text.content = "This is a much longer text that should definitely wrap to multiple lines"
2431
-
2432
- await renderOnce()
2433
- const updatedFrame = captureFrame()
2434
- expect(updatedFrame).toMatchSnapshot()
2435
- })
2436
- })
2437
-
2438
- describe("Mouse Scrolling", () => {
2439
- it("should receive mouse scroll events", async () => {
2440
- resize(20, 10)
2441
-
2442
- const longText = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"
2443
- const { text } = await createTextRenderable(currentRenderer, {
2444
- content: longText,
2445
- wrapMode: "none",
2446
- })
2447
-
2448
- await renderOnce()
2449
-
2450
- let scrollEventReceived = false
2451
- let scrollInfo: any = null
2452
-
2453
- // Override the handler to capture events
2454
- const originalHandler = text.onMouseScroll
2455
- text.onMouseScroll = (event: any) => {
2456
- scrollEventReceived = true
2457
- scrollInfo = event.scroll
2458
- // Call original handler
2459
- if (originalHandler) {
2460
- originalHandler.call(text, event)
2461
- }
2462
- }
2463
-
2464
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2465
- await renderOnce()
2466
-
2467
- expect(scrollEventReceived).toBe(true)
2468
- expect(scrollInfo).toBeDefined()
2469
- expect(scrollInfo?.direction).toBe("down")
2470
- })
2471
-
2472
- it("should handle mouse scroll events for vertical scrolling", async () => {
2473
- resize(20, 5)
2474
-
2475
- const longText = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"
2476
- const { text } = await createTextRenderable(currentRenderer, {
2477
- content: longText,
2478
- wrapMode: "none",
2479
- })
2480
-
2481
- await renderOnce()
2482
-
2483
- // Initially should be at scroll position 0
2484
- expect(text.scrollY).toBe(0)
2485
- expect(text.scrollX).toBe(0)
2486
-
2487
- // Scroll down (each scroll event typically moves by 1)
2488
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2489
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2490
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2491
- await renderOnce()
2492
-
2493
- expect(text.scrollY).toBe(3)
2494
-
2495
- // Scroll up
2496
- await currentMouse.scroll(text.x + 1, text.y + 1, "up")
2497
- await renderOnce()
2498
-
2499
- expect(text.scrollY).toBe(2)
2500
- })
2501
-
2502
- it("should handle mouse scroll events for horizontal scrolling with unwrapped text", async () => {
2503
- resize(80, 5)
2504
-
2505
- const wideText =
2506
- "This is a very long line that extends way beyond the visible area and should definitely need scrolling"
2507
- const { text } = await createTextRenderable(currentRenderer, {
2508
- content: wideText,
2509
- wrapMode: "none",
2510
- width: 20,
2511
- maxWidth: 20,
2512
- })
2513
-
2514
- await renderOnce()
2515
-
2516
- expect(text.scrollX).toBe(0)
2517
- expect(text.scrollY).toBe(0)
2518
-
2519
- // Scroll right
2520
- for (let i = 0; i < 5; i++) {
2521
- await currentMouse.scroll(text.x + 1, text.y, "right")
2522
- }
2523
- await renderOnce()
2524
-
2525
- expect(text.scrollX).toBe(5)
2526
-
2527
- // Scroll left
2528
- await currentMouse.scroll(text.x + 1, text.y, "left")
2529
- await currentMouse.scroll(text.x + 1, text.y, "left")
2530
- await renderOnce()
2531
-
2532
- expect(text.scrollX).toBe(3)
2533
- })
2534
-
2535
- it("should not allow horizontal scrolling when text is wrapped", async () => {
2536
- resize(20, 5)
2537
-
2538
- const longText =
2539
- "Line 1 text\nLine 2 text\nLine 3 text\nLine 4 text\nLine 5 text\nLine 6 text\nLine 7 text\nLine 8 text"
2540
- const { text } = await createTextRenderable(currentRenderer, {
2541
- content: longText,
2542
- wrapMode: "word",
2543
- width: 15,
2544
- height: 3, // Constrain height to enable vertical scrolling
2545
- })
2546
-
2547
- await renderOnce()
2548
-
2549
- // Try to scroll horizontally
2550
- for (let i = 0; i < 5; i++) {
2551
- await currentMouse.scroll(text.x + 1, text.y + 1, "right")
2552
- }
2553
- await renderOnce()
2554
-
2555
- // Should not scroll horizontally when wrapped
2556
- expect(text.scrollX).toBe(0)
2557
-
2558
- // But vertical scrolling should still work if there's content
2559
- if (text.maxScrollY > 0) {
2560
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2561
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2562
- await renderOnce()
2563
-
2564
- expect(text.scrollY).toBe(2)
2565
- }
2566
- })
2567
-
2568
- it("should clamp scroll position to valid bounds", async () => {
2569
- resize(20, 5)
2570
-
2571
- const shortText = "Line 1\nLine 2\nLine 3"
2572
- const { text } = await createTextRenderable(currentRenderer, {
2573
- content: shortText,
2574
- wrapMode: "none",
2575
- })
2576
-
2577
- await renderOnce()
2578
-
2579
- // Try to scroll beyond content
2580
- for (let i = 0; i < 10; i++) {
2581
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2582
- }
2583
- await renderOnce()
2584
-
2585
- // Should be clamped to maxScrollY
2586
- expect(text.scrollY).toBeLessThanOrEqual(text.maxScrollY)
2587
- expect(text.scrollY).toBeGreaterThanOrEqual(0)
2588
-
2589
- // Try to scroll up beyond 0
2590
- for (let i = 0; i < 20; i++) {
2591
- await currentMouse.scroll(text.x + 1, text.y + 1, "up")
2592
- }
2593
- await renderOnce()
2594
-
2595
- expect(text.scrollY).toBe(0)
2596
- })
2597
-
2598
- it("should expose scrollWidth and scrollHeight getters", async () => {
2599
- resize(20, 5)
2600
-
2601
- const text = "Line 1\nLine 2 with more content\nLine 3"
2602
- const { text: textRenderable } = await createTextRenderable(currentRenderer, {
2603
- content: text,
2604
- wrapMode: "none",
2605
- })
2606
-
2607
- await renderOnce()
2608
-
2609
- expect(textRenderable.scrollHeight).toBe(3) // 3 lines
2610
- expect(textRenderable.scrollWidth).toBeGreaterThan(0) // Max width of lines
2611
- })
2612
-
2613
- it("should calculate maxScrollY and maxScrollX correctly", async () => {
2614
- resize(20, 5)
2615
-
2616
- const text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8"
2617
- const { text: textRenderable } = await createTextRenderable(currentRenderer, {
2618
- content: text,
2619
- wrapMode: "none",
2620
- height: 5,
2621
- })
2622
-
2623
- await renderOnce()
2624
-
2625
- // maxScrollY should be scrollHeight - viewport height
2626
- expect(textRenderable.maxScrollY).toBe(Math.max(0, textRenderable.scrollHeight - textRenderable.height))
2627
-
2628
- // maxScrollX should be scrollWidth - viewport width
2629
- expect(textRenderable.maxScrollX).toBe(Math.max(0, textRenderable.scrollWidth - textRenderable.width))
2630
- })
2631
-
2632
- it("should update scroll position via setters", async () => {
2633
- resize(20, 5)
2634
-
2635
- const longText =
2636
- "Line 1 with some extra content\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"
2637
- const { text } = await createTextRenderable(currentRenderer, {
2638
- content: longText,
2639
- wrapMode: "none",
2640
- width: 20, // Constrain width
2641
- height: 5, // Constrain height
2642
- })
2643
-
2644
- await renderOnce()
2645
-
2646
- // Set scroll position directly
2647
- text.scrollY = 3
2648
- await renderOnce()
2649
-
2650
- expect(text.scrollY).toBe(3)
2651
-
2652
- // Set scrollX (only works if there's horizontal scrollable content)
2653
- if (text.maxScrollX > 0) {
2654
- text.scrollX = 2
2655
- await renderOnce()
2656
- expect(text.scrollX).toBe(2)
2657
- }
2658
- })
2659
- })
2660
- })