@fairyhunter13/opentui-core 0.1.88 → 0.1.90

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 (570) hide show
  1. package/dev/keypress-debug-renderer.ts +148 -0
  2. package/dev/keypress-debug.ts +43 -0
  3. package/dev/print-env-vars.ts +32 -0
  4. package/dev/test-tmux-graphics-334.sh +68 -0
  5. package/dev/thai-debug-test.ts +68 -0
  6. package/docs/development.md +141 -0
  7. package/docs/env-vars.md +140 -0
  8. package/docs/getting-started.md +353 -0
  9. package/docs/renderables-vs-constructs.md +159 -0
  10. package/docs/tree-sitter.md +311 -0
  11. package/package.json +61 -52
  12. package/scripts/build.ts +400 -0
  13. package/scripts/publish.ts +60 -0
  14. package/src/3d/SpriteResourceManager.ts +286 -0
  15. package/src/3d/SpriteUtils.ts +71 -0
  16. package/src/3d/TextureUtils.ts +196 -0
  17. package/src/3d/ThreeRenderable.ts +197 -0
  18. package/src/3d/WGPURenderer.ts +294 -0
  19. package/src/3d/animation/ExplodingSpriteEffect.ts +513 -0
  20. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +429 -0
  21. package/src/3d/animation/SpriteAnimator.ts +633 -0
  22. package/src/3d/animation/SpriteParticleGenerator.ts +435 -0
  23. package/src/3d/canvas.ts +464 -0
  24. package/src/3d/index.ts +12 -0
  25. package/src/3d/physics/PlanckPhysicsAdapter.ts +72 -0
  26. package/src/3d/physics/RapierPhysicsAdapter.ts +66 -0
  27. package/src/3d/physics/physics-interface.ts +31 -0
  28. package/src/3d/shaders/supersampling.wgsl +201 -0
  29. package/src/3d.ts +3 -0
  30. package/src/NativeSpanFeed.ts +300 -0
  31. package/src/Renderable.ts +1698 -0
  32. package/src/__snapshots__/buffer.test.ts.snap +28 -0
  33. package/src/animation/Timeline.test.ts +2709 -0
  34. package/src/animation/Timeline.ts +598 -0
  35. package/src/ansi.ts +18 -0
  36. package/src/benchmark/latest-all-bench-run.json +707 -0
  37. package/src/benchmark/latest-async-bench-run.json +336 -0
  38. package/src/benchmark/latest-default-bench-run.json +657 -0
  39. package/src/benchmark/latest-large-bench-run.json +707 -0
  40. package/src/benchmark/latest-quick-bench-run.json +207 -0
  41. package/src/benchmark/markdown-benchmark.ts +1804 -0
  42. package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
  43. package/src/benchmark/native-span-feed-benchmark.md +56 -0
  44. package/src/benchmark/native-span-feed-benchmark.ts +596 -0
  45. package/src/benchmark/native-span-feed-compare.ts +280 -0
  46. package/src/benchmark/renderer-benchmark.ts +754 -0
  47. package/src/benchmark/text-table-benchmark.ts +947 -0
  48. package/src/buffer.test.ts +291 -0
  49. package/src/buffer.ts +519 -0
  50. package/src/console.test.ts +612 -0
  51. package/src/console.ts +1255 -0
  52. package/src/edit-buffer.test.ts +1769 -0
  53. package/src/edit-buffer.ts +411 -0
  54. package/src/editor-view.test.ts +1032 -0
  55. package/src/editor-view.ts +284 -0
  56. package/src/examples/ascii-font-selection-demo.ts +245 -0
  57. package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
  58. package/src/examples/assets/concrete.png +0 -0
  59. package/src/examples/assets/crate.png +0 -0
  60. package/src/examples/assets/crate_emissive.png +0 -0
  61. package/src/examples/assets/forrest_background.png +0 -0
  62. package/src/examples/assets/hast-example.json +1018 -0
  63. package/src/examples/assets/heart.png +0 -0
  64. package/src/examples/assets/main_char_heavy_attack.png +0 -0
  65. package/src/examples/assets/main_char_idle.png +0 -0
  66. package/src/examples/assets/main_char_jump_end.png +0 -0
  67. package/src/examples/assets/main_char_jump_landing.png +0 -0
  68. package/src/examples/assets/main_char_jump_start.png +0 -0
  69. package/src/examples/assets/main_char_run_loop.png +0 -0
  70. package/src/examples/assets/roughness_map.jpg +0 -0
  71. package/src/examples/build.ts +115 -0
  72. package/src/examples/code-demo.ts +584 -0
  73. package/src/examples/console-demo.ts +358 -0
  74. package/src/examples/core-plugin-slots-demo.ts +759 -0
  75. package/src/examples/diff-demo.ts +699 -0
  76. package/src/examples/draggable-three-demo.ts +259 -0
  77. package/src/examples/editor-demo.ts +322 -0
  78. package/src/examples/extmarks-demo.ts +204 -0
  79. package/src/examples/focus-restore-demo.ts +310 -0
  80. package/src/examples/fonts.ts +245 -0
  81. package/src/examples/fractal-shader-demo.ts +268 -0
  82. package/src/examples/framebuffer-demo.ts +674 -0
  83. package/src/examples/full-unicode-demo.ts +181 -0
  84. package/src/examples/golden-star-demo.ts +933 -0
  85. package/src/examples/grayscale-buffer-demo.ts +249 -0
  86. package/src/examples/hast-syntax-highlighting-demo.ts +129 -0
  87. package/src/examples/index.ts +925 -0
  88. package/src/examples/input-demo.ts +377 -0
  89. package/src/examples/input-select-layout-demo.ts +425 -0
  90. package/src/examples/install.sh +143 -0
  91. package/src/examples/keypress-debug-demo.ts +452 -0
  92. package/src/examples/lib/HexList.ts +122 -0
  93. package/src/examples/lib/PaletteGrid.ts +125 -0
  94. package/src/examples/lib/standalone-keys.ts +25 -0
  95. package/src/examples/lib/tab-controller.ts +243 -0
  96. package/src/examples/lights-phong-demo.ts +290 -0
  97. package/src/examples/link-demo.ts +220 -0
  98. package/src/examples/live-state-demo.ts +480 -0
  99. package/src/examples/markdown-demo.ts +620 -0
  100. package/src/examples/mouse-interaction-demo.ts +428 -0
  101. package/src/examples/nested-zindex-demo.ts +357 -0
  102. package/src/examples/opacity-example.ts +235 -0
  103. package/src/examples/opentui-demo.ts +1057 -0
  104. package/src/examples/physx-planck-2d-demo.ts +507 -0
  105. package/src/examples/physx-rapier-2d-demo.ts +526 -0
  106. package/src/examples/relative-positioning-demo.ts +323 -0
  107. package/src/examples/scroll-example.ts +214 -0
  108. package/src/examples/scrollbox-mouse-test.ts +112 -0
  109. package/src/examples/scrollbox-overlay-hit-test.ts +206 -0
  110. package/src/examples/select-demo.ts +237 -0
  111. package/src/examples/shader-cube-demo.ts +772 -0
  112. package/src/examples/simple-layout-example.ts +591 -0
  113. package/src/examples/slider-demo.ts +617 -0
  114. package/src/examples/split-mode-demo.ts +445 -0
  115. package/src/examples/sprite-animation-demo.ts +443 -0
  116. package/src/examples/sprite-particle-generator-demo.ts +486 -0
  117. package/src/examples/static-sprite-demo.ts +193 -0
  118. package/src/examples/sticky-scroll-example.ts +308 -0
  119. package/src/examples/styled-text-demo.ts +282 -0
  120. package/src/examples/tab-select-demo.ts +219 -0
  121. package/src/examples/terminal-title.ts +29 -0
  122. package/src/examples/terminal.ts +305 -0
  123. package/src/examples/text-node-demo.ts +416 -0
  124. package/src/examples/text-selection-demo.ts +377 -0
  125. package/src/examples/text-table-demo.ts +503 -0
  126. package/src/examples/text-truncation-demo.ts +481 -0
  127. package/src/examples/text-wrap.ts +757 -0
  128. package/src/examples/texture-loading-demo.ts +259 -0
  129. package/src/examples/timeline-example.ts +670 -0
  130. package/src/examples/transparency-demo.ts +241 -0
  131. package/src/examples/vnode-composition-demo.ts +404 -0
  132. package/src/index.ts +22 -0
  133. package/src/lib/KeyHandler.integration.test.ts +292 -0
  134. package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
  135. package/src/lib/KeyHandler.test.ts +662 -0
  136. package/src/lib/KeyHandler.ts +222 -0
  137. package/src/lib/RGBA.test.ts +984 -0
  138. package/src/lib/RGBA.ts +204 -0
  139. package/src/lib/ascii.font.ts +330 -0
  140. package/src/lib/border.test.ts +83 -0
  141. package/src/lib/border.ts +168 -0
  142. package/src/lib/bunfs.test.ts +27 -0
  143. package/src/lib/bunfs.ts +18 -0
  144. package/src/lib/clipboard.test.ts +41 -0
  145. package/src/lib/clipboard.ts +47 -0
  146. package/src/lib/clock.ts +31 -0
  147. package/src/lib/data-paths.test.ts +133 -0
  148. package/src/lib/data-paths.ts +109 -0
  149. package/src/lib/debounce.ts +106 -0
  150. package/src/lib/detect-links.test.ts +98 -0
  151. package/src/lib/detect-links.ts +56 -0
  152. package/src/lib/env.test.ts +228 -0
  153. package/src/lib/env.ts +209 -0
  154. package/src/lib/extmarks-history.ts +51 -0
  155. package/src/lib/extmarks-multiwidth.test.ts +322 -0
  156. package/src/lib/extmarks.test.ts +3457 -0
  157. package/src/lib/extmarks.ts +843 -0
  158. package/src/lib/fonts/block.json +405 -0
  159. package/src/lib/fonts/grid.json +265 -0
  160. package/src/lib/fonts/huge.json +741 -0
  161. package/src/lib/fonts/pallet.json +314 -0
  162. package/src/lib/fonts/shade.json +591 -0
  163. package/src/lib/fonts/slick.json +321 -0
  164. package/src/lib/fonts/tiny.json +69 -0
  165. package/src/lib/hast-styled-text.ts +59 -0
  166. package/src/lib/index.ts +21 -0
  167. package/src/lib/keymapping.test.ts +280 -0
  168. package/src/lib/keymapping.ts +87 -0
  169. package/src/lib/objects-in-viewport.test.ts +787 -0
  170. package/src/lib/objects-in-viewport.ts +153 -0
  171. package/src/lib/output.capture.ts +58 -0
  172. package/src/lib/parse.keypress-kitty.protocol.test.ts +340 -0
  173. package/src/lib/parse.keypress-kitty.test.ts +663 -0
  174. package/src/lib/parse.keypress-kitty.ts +439 -0
  175. package/src/lib/parse.keypress.test.ts +1849 -0
  176. package/src/lib/parse.keypress.ts +397 -0
  177. package/src/lib/parse.mouse.test.ts +552 -0
  178. package/src/lib/parse.mouse.ts +232 -0
  179. package/src/lib/paste.ts +16 -0
  180. package/src/lib/queue.ts +65 -0
  181. package/src/lib/renderable.validations.test.ts +87 -0
  182. package/src/lib/renderable.validations.ts +83 -0
  183. package/src/lib/scroll-acceleration.ts +98 -0
  184. package/src/lib/selection.ts +240 -0
  185. package/src/lib/singleton.ts +28 -0
  186. package/src/lib/stdin-parser.test.ts +1676 -0
  187. package/src/lib/stdin-parser.ts +1248 -0
  188. package/src/lib/styled-text.ts +178 -0
  189. package/src/lib/terminal-capability-detection.test.ts +202 -0
  190. package/src/lib/terminal-capability-detection.ts +79 -0
  191. package/src/lib/terminal-palette.test.ts +878 -0
  192. package/src/lib/terminal-palette.ts +383 -0
  193. package/src/lib/tree-sitter/assets/README.md +118 -0
  194. package/src/lib/tree-sitter/assets/update.ts +331 -0
  195. package/src/lib/tree-sitter/assets.d.ts +9 -0
  196. package/src/lib/tree-sitter/cache.test.ts +270 -0
  197. package/src/lib/tree-sitter/client.test.ts +1061 -0
  198. package/src/lib/tree-sitter/client.ts +615 -0
  199. package/src/lib/tree-sitter/default-parsers.ts +80 -0
  200. package/src/lib/tree-sitter/download-utils.ts +148 -0
  201. package/src/lib/tree-sitter/index.ts +28 -0
  202. package/src/lib/tree-sitter/parser.worker.ts +1001 -0
  203. package/src/lib/tree-sitter/parsers-config.ts +75 -0
  204. package/src/lib/tree-sitter/resolve-ft.ts +62 -0
  205. package/src/lib/tree-sitter/types.ts +81 -0
  206. package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
  207. package/src/lib/tree-sitter-styled-text.ts +306 -0
  208. package/src/lib/validate-dir-name.ts +55 -0
  209. package/src/lib/yoga.options.test.ts +628 -0
  210. package/src/lib/yoga.options.ts +346 -0
  211. package/src/plugins/core-slot.ts +579 -0
  212. package/src/plugins/registry.ts +377 -0
  213. package/src/plugins/types.ts +46 -0
  214. package/src/post/filters.ts +888 -0
  215. package/src/renderables/ASCIIFont.ts +219 -0
  216. package/src/renderables/Box.test.ts +160 -0
  217. package/src/renderables/Box.ts +295 -0
  218. package/src/renderables/Code.test.ts +2062 -0
  219. package/src/renderables/Code.ts +357 -0
  220. package/src/renderables/Diff.regression.test.ts +226 -0
  221. package/src/renderables/Diff.test.ts +3027 -0
  222. package/src/renderables/Diff.ts +1209 -0
  223. package/src/renderables/EditBufferRenderable.ts +764 -0
  224. package/src/renderables/FrameBuffer.ts +47 -0
  225. package/src/renderables/Input.test.ts +1228 -0
  226. package/src/renderables/Input.ts +245 -0
  227. package/src/renderables/LineNumberRenderable.ts +675 -0
  228. package/src/renderables/Markdown.ts +1106 -0
  229. package/src/renderables/ScrollBar.ts +422 -0
  230. package/src/renderables/ScrollBox.ts +883 -0
  231. package/src/renderables/Select.test.ts +1010 -0
  232. package/src/renderables/Select.ts +523 -0
  233. package/src/renderables/Slider.test.ts +456 -0
  234. package/src/renderables/Slider.ts +347 -0
  235. package/src/renderables/TabSelect.test.ts +197 -0
  236. package/src/renderables/TabSelect.ts +455 -0
  237. package/src/renderables/Text.selection-buffer.test.ts +123 -0
  238. package/src/renderables/Text.test.ts +2660 -0
  239. package/src/renderables/Text.ts +147 -0
  240. package/src/renderables/TextBufferRenderable.ts +518 -0
  241. package/src/renderables/TextNode.test.ts +1058 -0
  242. package/src/renderables/TextNode.ts +325 -0
  243. package/src/renderables/TextTable.test.ts +1421 -0
  244. package/src/renderables/TextTable.ts +1344 -0
  245. package/src/renderables/Textarea.ts +732 -0
  246. package/src/renderables/TimeToFirstDraw.ts +89 -0
  247. package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
  248. package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
  249. package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
  250. package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
  251. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
  252. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +816 -0
  253. package/src/renderables/__tests__/LineNumberRenderable.test.ts +1787 -0
  254. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
  255. package/src/renderables/__tests__/Markdown.test.ts +2287 -0
  256. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
  257. package/src/renderables/__tests__/Textarea.buffer.test.ts +682 -0
  258. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
  259. package/src/renderables/__tests__/Textarea.editing.test.ts +2041 -0
  260. package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
  261. package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
  262. package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
  263. package/src/renderables/__tests__/Textarea.keybinding.test.ts +3149 -0
  264. package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
  265. package/src/renderables/__tests__/Textarea.rendering.test.ts +1864 -0
  266. package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
  267. package/src/renderables/__tests__/Textarea.selection.test.ts +1590 -0
  268. package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
  269. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
  270. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
  271. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
  272. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
  273. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
  274. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
  275. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
  276. package/src/renderables/__tests__/markdown-parser.test.ts +217 -0
  277. package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
  278. package/src/renderables/composition/README.md +8 -0
  279. package/src/renderables/composition/VRenderable.ts +32 -0
  280. package/src/renderables/composition/constructs.ts +127 -0
  281. package/src/renderables/composition/vnode.ts +289 -0
  282. package/src/renderables/index.ts +22 -0
  283. package/src/renderables/markdown-parser.ts +66 -0
  284. package/src/renderer.ts +2363 -0
  285. package/src/runtime-plugin-support.ts +39 -0
  286. package/src/runtime-plugin.ts +144 -0
  287. package/src/syntax-style.test.ts +841 -0
  288. package/src/syntax-style.ts +264 -0
  289. package/src/testing/README.md +210 -0
  290. package/src/testing/capture-spans.test.ts +194 -0
  291. package/src/testing/integration.test.ts +276 -0
  292. package/src/testing/manual-clock.ts +106 -0
  293. package/src/testing/mock-keys.test.ts +1356 -0
  294. package/src/testing/mock-keys.ts +449 -0
  295. package/src/testing/mock-mouse.test.ts +218 -0
  296. package/src/testing/mock-mouse.ts +247 -0
  297. package/src/testing/mock-tree-sitter-client.ts +73 -0
  298. package/src/testing/spy.ts +13 -0
  299. package/src/testing/test-recorder.test.ts +415 -0
  300. package/src/testing/test-recorder.ts +145 -0
  301. package/src/testing/test-renderer.ts +116 -0
  302. package/src/testing.ts +7 -0
  303. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
  304. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
  305. package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
  306. package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
  307. package/src/tests/allocator-stats.test.ts +38 -0
  308. package/src/tests/destroy-during-render.test.ts +200 -0
  309. package/src/tests/hover-cursor.test.ts +98 -0
  310. package/src/tests/native-span-feed-async.test.ts +173 -0
  311. package/src/tests/native-span-feed-close.test.ts +120 -0
  312. package/src/tests/native-span-feed-coverage.test.ts +227 -0
  313. package/src/tests/native-span-feed-edge-cases.test.ts +352 -0
  314. package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
  315. package/src/tests/opacity.test.ts +123 -0
  316. package/src/tests/renderable.snapshot.test.ts +524 -0
  317. package/src/tests/renderable.test.ts +1281 -0
  318. package/src/tests/renderer.console-startup.test.ts +65 -0
  319. package/src/tests/renderer.control.test.ts +364 -0
  320. package/src/tests/renderer.core-slot-binding.test.ts +952 -0
  321. package/src/tests/renderer.cursor.test.ts +26 -0
  322. package/src/tests/renderer.destroy-during-render.test.ts +110 -0
  323. package/src/tests/renderer.focus-restore.test.ts +228 -0
  324. package/src/tests/renderer.focus.test.ts +251 -0
  325. package/src/tests/renderer.idle.test.ts +219 -0
  326. package/src/tests/renderer.input.test.ts +2145 -0
  327. package/src/tests/renderer.kitty-flags.test.ts +195 -0
  328. package/src/tests/renderer.mouse.test.ts +1269 -0
  329. package/src/tests/renderer.palette.test.ts +629 -0
  330. package/src/tests/renderer.selection.test.ts +49 -0
  331. package/src/tests/renderer.slot-registry.test.ts +649 -0
  332. package/src/tests/renderer.useMouse.test.ts +50 -0
  333. package/src/tests/runtime-plugin-support.fixture.ts +11 -0
  334. package/src/tests/runtime-plugin-support.test.ts +28 -0
  335. package/src/tests/runtime-plugin.fixture.ts +40 -0
  336. package/src/tests/runtime-plugin.test.ts +190 -0
  337. package/src/tests/scrollbox-culling-bug.test.ts +114 -0
  338. package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
  339. package/src/tests/scrollbox-hitgrid.test.ts +909 -0
  340. package/src/tests/scrollbox.test.ts +1530 -0
  341. package/src/tests/wrap-resize-perf.test.ts +229 -0
  342. package/src/tests/yoga-setters.test.ts +921 -0
  343. package/src/text-buffer-view.test.ts +705 -0
  344. package/src/text-buffer-view.ts +189 -0
  345. package/src/text-buffer.test.ts +347 -0
  346. package/src/text-buffer.ts +250 -0
  347. package/src/types.ts +152 -0
  348. package/src/utils.ts +88 -0
  349. package/src/zig/ansi.zig +268 -0
  350. package/src/zig/bench/README.md +50 -0
  351. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +887 -0
  352. package/src/zig/bench/edit-buffer_bench.zig +476 -0
  353. package/src/zig/bench/native-span-feed_bench.zig +100 -0
  354. package/src/zig/bench/rope-markers_bench.zig +713 -0
  355. package/src/zig/bench/rope_bench.zig +514 -0
  356. package/src/zig/bench/styled-text_bench.zig +470 -0
  357. package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
  358. package/src/zig/bench/text-buffer-view_bench.zig +459 -0
  359. package/src/zig/bench/text-chunk-graphemes_bench.zig +273 -0
  360. package/src/zig/bench/utf8_bench.zig +799 -0
  361. package/src/zig/bench-utils.zig +431 -0
  362. package/src/zig/bench.zig +217 -0
  363. package/src/zig/buffer.zig +2223 -0
  364. package/src/zig/build.zig +289 -0
  365. package/src/zig/build.zig.zon +16 -0
  366. package/src/zig/edit-buffer.zig +825 -0
  367. package/src/zig/editor-view.zig +802 -0
  368. package/src/zig/event-bus.zig +13 -0
  369. package/src/zig/event-emitter.zig +65 -0
  370. package/src/zig/file-logger.zig +92 -0
  371. package/src/zig/grapheme.zig +599 -0
  372. package/src/zig/lib.zig +1834 -0
  373. package/src/zig/link.zig +333 -0
  374. package/src/zig/logger.zig +43 -0
  375. package/src/zig/mem-registry.zig +125 -0
  376. package/src/zig/native-span-feed-bench-lib.zig +7 -0
  377. package/src/zig/native-span-feed.zig +708 -0
  378. package/src/zig/renderer.zig +1386 -0
  379. package/src/zig/rope.zig +1220 -0
  380. package/src/zig/syntax-style.zig +161 -0
  381. package/src/zig/terminal.zig +975 -0
  382. package/src/zig/test.zig +70 -0
  383. package/src/zig/tests/README.md +18 -0
  384. package/src/zig/tests/buffer_test.zig +2526 -0
  385. package/src/zig/tests/edit-buffer-history_test.zig +271 -0
  386. package/src/zig/tests/edit-buffer_test.zig +1689 -0
  387. package/src/zig/tests/editor-view_test.zig +3299 -0
  388. package/src/zig/tests/event-emitter_test.zig +249 -0
  389. package/src/zig/tests/grapheme_test.zig +1304 -0
  390. package/src/zig/tests/link_test.zig +190 -0
  391. package/src/zig/tests/mem-registry_test.zig +473 -0
  392. package/src/zig/tests/memory_leak_regression_test.zig +159 -0
  393. package/src/zig/tests/native-span-feed_test.zig +1264 -0
  394. package/src/zig/tests/renderer_test.zig +1010 -0
  395. package/src/zig/tests/rope-nested_test.zig +712 -0
  396. package/src/zig/tests/rope_fuzz_test.zig +238 -0
  397. package/src/zig/tests/rope_test.zig +2362 -0
  398. package/src/zig/tests/segment-merge.test.zig +148 -0
  399. package/src/zig/tests/syntax-style_test.zig +557 -0
  400. package/src/zig/tests/terminal_test.zig +719 -0
  401. package/src/zig/tests/text-buffer-drawing_test.zig +3237 -0
  402. package/src/zig/tests/text-buffer-highlights_test.zig +666 -0
  403. package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
  404. package/src/zig/tests/text-buffer-segment_test.zig +320 -0
  405. package/src/zig/tests/text-buffer-selection_test.zig +1035 -0
  406. package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
  407. package/src/zig/tests/text-buffer-view_test.zig +3649 -0
  408. package/src/zig/tests/text-buffer_test.zig +2191 -0
  409. package/src/zig/tests/unicode-width-map.zon +3909 -0
  410. package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
  411. package/src/zig/tests/utf8_test.zig +4057 -0
  412. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
  413. package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
  414. package/src/zig/tests/word-wrap-editing_test.zig +498 -0
  415. package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
  416. package/src/zig/text-buffer-iterators.zig +499 -0
  417. package/src/zig/text-buffer-segment.zig +404 -0
  418. package/src/zig/text-buffer-view.zig +1371 -0
  419. package/src/zig/text-buffer.zig +1180 -0
  420. package/src/zig/utf8.zig +1948 -0
  421. package/src/zig/utils.zig +9 -0
  422. package/src/zig-structs.ts +261 -0
  423. package/src/zig.ts +3843 -0
  424. package/tsconfig.build.json +22 -0
  425. package/tsconfig.json +28 -0
  426. package/3d/SpriteResourceManager.d.ts +0 -74
  427. package/3d/SpriteUtils.d.ts +0 -13
  428. package/3d/TextureUtils.d.ts +0 -24
  429. package/3d/ThreeRenderable.d.ts +0 -40
  430. package/3d/WGPURenderer.d.ts +0 -61
  431. package/3d/animation/ExplodingSpriteEffect.d.ts +0 -71
  432. package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +0 -76
  433. package/3d/animation/SpriteAnimator.d.ts +0 -124
  434. package/3d/animation/SpriteParticleGenerator.d.ts +0 -62
  435. package/3d/canvas.d.ts +0 -44
  436. package/3d/index.d.ts +0 -12
  437. package/3d/physics/PlanckPhysicsAdapter.d.ts +0 -19
  438. package/3d/physics/RapierPhysicsAdapter.d.ts +0 -19
  439. package/3d/physics/physics-interface.d.ts +0 -27
  440. package/3d.d.ts +0 -2
  441. package/3d.js +0 -34042
  442. package/3d.js.map +0 -155
  443. package/LICENSE +0 -21
  444. package/NativeSpanFeed.d.ts +0 -41
  445. package/Renderable.d.ts +0 -334
  446. package/animation/Timeline.d.ts +0 -126
  447. package/ansi.d.ts +0 -13
  448. package/buffer.d.ts +0 -107
  449. package/console.d.ts +0 -143
  450. package/edit-buffer.d.ts +0 -98
  451. package/editor-view.d.ts +0 -73
  452. package/index-e4hzc2j2.js +0 -113
  453. package/index-e4hzc2j2.js.map +0 -10
  454. package/index-nkrr8a4c.js +0 -18415
  455. package/index-nkrr8a4c.js.map +0 -64
  456. package/index-nyw5p3ep.js +0 -12619
  457. package/index-nyw5p3ep.js.map +0 -43
  458. package/index.d.ts +0 -21
  459. package/index.js +0 -430
  460. package/index.js.map +0 -9
  461. package/lib/KeyHandler.d.ts +0 -61
  462. package/lib/RGBA.d.ts +0 -25
  463. package/lib/ascii.font.d.ts +0 -508
  464. package/lib/border.d.ts +0 -49
  465. package/lib/bunfs.d.ts +0 -7
  466. package/lib/clipboard.d.ts +0 -17
  467. package/lib/clock.d.ts +0 -15
  468. package/lib/data-paths.d.ts +0 -26
  469. package/lib/debounce.d.ts +0 -42
  470. package/lib/detect-links.d.ts +0 -6
  471. package/lib/env.d.ts +0 -42
  472. package/lib/extmarks-history.d.ts +0 -17
  473. package/lib/extmarks.d.ts +0 -89
  474. package/lib/hast-styled-text.d.ts +0 -17
  475. package/lib/index.d.ts +0 -21
  476. package/lib/keymapping.d.ts +0 -25
  477. package/lib/objects-in-viewport.d.ts +0 -24
  478. package/lib/output.capture.d.ts +0 -24
  479. package/lib/parse.keypress-kitty.d.ts +0 -2
  480. package/lib/parse.keypress.d.ts +0 -26
  481. package/lib/parse.mouse.d.ts +0 -30
  482. package/lib/paste.d.ts +0 -7
  483. package/lib/queue.d.ts +0 -15
  484. package/lib/renderable.validations.d.ts +0 -12
  485. package/lib/scroll-acceleration.d.ts +0 -43
  486. package/lib/selection.d.ts +0 -63
  487. package/lib/singleton.d.ts +0 -7
  488. package/lib/stdin-parser.d.ts +0 -76
  489. package/lib/styled-text.d.ts +0 -63
  490. package/lib/terminal-capability-detection.d.ts +0 -30
  491. package/lib/terminal-palette.d.ts +0 -50
  492. package/lib/tree-sitter/assets/update.d.ts +0 -11
  493. package/lib/tree-sitter/client.d.ts +0 -47
  494. package/lib/tree-sitter/default-parsers.d.ts +0 -2
  495. package/lib/tree-sitter/download-utils.d.ts +0 -21
  496. package/lib/tree-sitter/index.d.ts +0 -8
  497. package/lib/tree-sitter/parser.worker.d.ts +0 -1
  498. package/lib/tree-sitter/parsers-config.d.ts +0 -38
  499. package/lib/tree-sitter/resolve-ft.d.ts +0 -2
  500. package/lib/tree-sitter/types.d.ts +0 -81
  501. package/lib/tree-sitter-styled-text.d.ts +0 -14
  502. package/lib/validate-dir-name.d.ts +0 -1
  503. package/lib/yoga.options.d.ts +0 -32
  504. package/parser.worker.js +0 -869
  505. package/parser.worker.js.map +0 -12
  506. package/plugins/core-slot.d.ts +0 -72
  507. package/plugins/registry.d.ts +0 -38
  508. package/plugins/types.d.ts +0 -34
  509. package/post/filters.d.ts +0 -105
  510. package/renderables/ASCIIFont.d.ts +0 -52
  511. package/renderables/Box.d.ts +0 -72
  512. package/renderables/Code.d.ts +0 -78
  513. package/renderables/Diff.d.ts +0 -142
  514. package/renderables/EditBufferRenderable.d.ts +0 -162
  515. package/renderables/FrameBuffer.d.ts +0 -16
  516. package/renderables/Input.d.ts +0 -67
  517. package/renderables/LineNumberRenderable.d.ts +0 -74
  518. package/renderables/Markdown.d.ts +0 -173
  519. package/renderables/ScrollBar.d.ts +0 -77
  520. package/renderables/ScrollBox.d.ts +0 -124
  521. package/renderables/Select.d.ts +0 -115
  522. package/renderables/Slider.d.ts +0 -44
  523. package/renderables/TabSelect.d.ts +0 -96
  524. package/renderables/Text.d.ts +0 -36
  525. package/renderables/TextBufferRenderable.d.ts +0 -105
  526. package/renderables/TextNode.d.ts +0 -91
  527. package/renderables/TextTable.d.ts +0 -140
  528. package/renderables/Textarea.d.ts +0 -114
  529. package/renderables/TimeToFirstDraw.d.ts +0 -24
  530. package/renderables/__tests__/renderable-test-utils.d.ts +0 -12
  531. package/renderables/composition/VRenderable.d.ts +0 -16
  532. package/renderables/composition/constructs.d.ts +0 -35
  533. package/renderables/composition/vnode.d.ts +0 -46
  534. package/renderables/index.d.ts +0 -22
  535. package/renderables/markdown-parser.d.ts +0 -10
  536. package/renderer.d.ts +0 -388
  537. package/runtime-plugin-support.d.ts +0 -3
  538. package/runtime-plugin-support.js +0 -29
  539. package/runtime-plugin-support.js.map +0 -10
  540. package/runtime-plugin.d.ts +0 -11
  541. package/runtime-plugin.js +0 -16
  542. package/runtime-plugin.js.map +0 -9
  543. package/syntax-style.d.ts +0 -54
  544. package/testing/manual-clock.d.ts +0 -16
  545. package/testing/mock-keys.d.ts +0 -81
  546. package/testing/mock-mouse.d.ts +0 -38
  547. package/testing/mock-tree-sitter-client.d.ts +0 -23
  548. package/testing/spy.d.ts +0 -7
  549. package/testing/test-recorder.d.ts +0 -61
  550. package/testing/test-renderer.d.ts +0 -23
  551. package/testing.d.ts +0 -6
  552. package/testing.js +0 -675
  553. package/testing.js.map +0 -15
  554. package/text-buffer-view.d.ts +0 -42
  555. package/text-buffer.d.ts +0 -67
  556. package/types.d.ts +0 -131
  557. package/utils.d.ts +0 -14
  558. package/zig-structs.d.ts +0 -155
  559. package/zig.d.ts +0 -351
  560. /package/{assets → src/lib/tree-sitter/assets}/javascript/highlights.scm +0 -0
  561. /package/{assets → src/lib/tree-sitter/assets}/javascript/tree-sitter-javascript.wasm +0 -0
  562. /package/{assets → src/lib/tree-sitter/assets}/markdown/highlights.scm +0 -0
  563. /package/{assets → src/lib/tree-sitter/assets}/markdown/injections.scm +0 -0
  564. /package/{assets → src/lib/tree-sitter/assets}/markdown/tree-sitter-markdown.wasm +0 -0
  565. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/highlights.scm +0 -0
  566. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  567. /package/{assets → src/lib/tree-sitter/assets}/typescript/highlights.scm +0 -0
  568. /package/{assets → src/lib/tree-sitter/assets}/typescript/tree-sitter-typescript.wasm +0 -0
  569. /package/{assets → src/lib/tree-sitter/assets}/zig/highlights.scm +0 -0
  570. /package/{assets → src/lib/tree-sitter/assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -0,0 +1,2363 @@
1
+ import { ANSI } from "./ansi.js"
2
+ import { Renderable, RootRenderable } from "./Renderable.js"
3
+ import {
4
+ DebugOverlayCorner,
5
+ type CursorStyleOptions,
6
+ type MousePointerStyle,
7
+ type RenderContext,
8
+ type ThemeMode,
9
+ type ViewportBounds,
10
+ type WidthMethod,
11
+ } from "./types.js"
12
+ import { RGBA, parseColor, type ColorInput } from "./lib/RGBA.js"
13
+ import type { Pointer } from "bun:ffi"
14
+ import { OptimizedBuffer } from "./buffer.js"
15
+ import { resolveRenderLib, type RenderLib } from "./zig.js"
16
+ import { TerminalConsole, type ConsoleOptions, capture } from "./console.js"
17
+ import { type MouseEventType, type RawMouseEvent, type ScrollInfo } from "./lib/parse.mouse.js"
18
+ import { Selection } from "./lib/selection.js"
19
+ import { Clipboard, type ClipboardTarget } from "./lib/clipboard.js"
20
+ import { EventEmitter } from "events"
21
+ import { destroySingleton, hasSingleton, singleton } from "./lib/singleton.js"
22
+ import { getObjectsInViewport } from "./lib/objects-in-viewport.js"
23
+ import { KeyHandler, InternalKeyHandler } from "./lib/KeyHandler.js"
24
+ import { env, registerEnvVar } from "./lib/env.js"
25
+ import { getTreeSitterClient } from "./lib/tree-sitter/index.js"
26
+ import {
27
+ createTerminalPalette,
28
+ type TerminalPaletteDetector,
29
+ type TerminalColors,
30
+ type GetPaletteOptions,
31
+ } from "./lib/terminal-palette.js"
32
+ import {
33
+ isCapabilityResponse,
34
+ isPixelResolutionResponse,
35
+ parsePixelResolution,
36
+ } from "./lib/terminal-capability-detection.js"
37
+ import { type Clock, type TimerHandle, SystemClock } from "./lib/clock.js"
38
+ import { StdinParser, type StdinEvent } from "./lib/stdin-parser.js"
39
+
40
+ registerEnvVar({
41
+ name: "OTUI_DUMP_CAPTURES",
42
+ description: "Dump captured output when the renderer exits.",
43
+ type: "boolean",
44
+ default: false,
45
+ })
46
+
47
+ registerEnvVar({
48
+ name: "OTUI_NO_NATIVE_RENDER",
49
+ description: "Disable native rendering. This will not actually output ansi and is useful for debugging.",
50
+ type: "boolean",
51
+ default: false,
52
+ })
53
+
54
+ registerEnvVar({
55
+ name: "OTUI_USE_ALTERNATE_SCREEN",
56
+ description: "Whether to use the console. Will not capture console output if set to false.",
57
+ type: "boolean",
58
+ default: true,
59
+ })
60
+
61
+ registerEnvVar({
62
+ name: "OTUI_OVERRIDE_STDOUT",
63
+ description: "Override the stdout stream. This is useful for debugging.",
64
+ type: "boolean",
65
+ default: true,
66
+ })
67
+
68
+ registerEnvVar({
69
+ name: "OTUI_DEBUG",
70
+ description: "Enable debug mode to capture all raw input for debugging purposes.",
71
+ type: "boolean",
72
+ default: false,
73
+ })
74
+
75
+ registerEnvVar({
76
+ name: "OTUI_SHOW_STATS",
77
+ description: "Show the debug overlay at startup.",
78
+ type: "boolean",
79
+ default: false,
80
+ })
81
+
82
+ export interface CliRendererConfig {
83
+ stdin?: NodeJS.ReadStream
84
+ stdout?: NodeJS.WriteStream
85
+ remote?: boolean
86
+ testing?: boolean
87
+ exitOnCtrlC?: boolean
88
+ exitSignals?: NodeJS.Signals[]
89
+ forwardEnvKeys?: string[]
90
+ debounceDelay?: number
91
+ targetFps?: number
92
+ maxFps?: number
93
+ memorySnapshotInterval?: number
94
+ useThread?: boolean
95
+ gatherStats?: boolean
96
+ maxStatSamples?: number
97
+ consoleOptions?: Omit<ConsoleOptions, "clock">
98
+ postProcessFns?: ((buffer: OptimizedBuffer, deltaTime: number) => void)[]
99
+ enableMouseMovement?: boolean
100
+ useMouse?: boolean
101
+ autoFocus?: boolean
102
+ useAlternateScreen?: boolean
103
+ useConsole?: boolean
104
+ experimental_splitHeight?: number
105
+ useKittyKeyboard?: KittyKeyboardOptions | null
106
+ backgroundColor?: ColorInput
107
+ openConsoleOnError?: boolean
108
+ prependInputHandlers?: ((sequence: string) => boolean)[]
109
+ stdinParserMaxBufferBytes?: number
110
+ clock?: Clock
111
+ onDestroy?: () => void
112
+ }
113
+
114
+ export type PixelResolution = {
115
+ width: number
116
+ height: number
117
+ }
118
+
119
+ const DEFAULT_FORWARDED_ENV_KEYS = [
120
+ "TMUX",
121
+ "TERM",
122
+ "OPENTUI_GRAPHICS",
123
+ "TERM_PROGRAM",
124
+ "TERM_PROGRAM_VERSION",
125
+ "ALACRITTY_SOCKET",
126
+ "ALACRITTY_LOG",
127
+ "COLORTERM",
128
+ "TERMUX_VERSION",
129
+ "VHS_RECORD",
130
+ "OPENTUI_FORCE_WCWIDTH",
131
+ "OPENTUI_FORCE_UNICODE",
132
+ "OPENTUI_FORCE_NOZWJ",
133
+ "OPENTUI_FORCE_EXPLICIT_WIDTH",
134
+ "WT_SESSION",
135
+ "STY",
136
+ ] as const
137
+
138
+ // Kitty keyboard protocol flags
139
+ // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
140
+ const KITTY_FLAG_DISAMBIGUATE = 0b1 // Report disambiguated escape codes
141
+ const KITTY_FLAG_EVENT_TYPES = 0b10 // Report event types (press/repeat/release)
142
+ const KITTY_FLAG_ALTERNATE_KEYS = 0b100 // Report alternate keys (e.g., numpad vs regular)
143
+ const KITTY_FLAG_ALL_KEYS_AS_ESCAPES = 0b1000 // Report all keys as escape codes
144
+ const KITTY_FLAG_REPORT_TEXT = 0b10000 // Report text associated with key events
145
+
146
+ const DEFAULT_STDIN_PARSER_MAX_BUFFER_BYTES = 64 * 1024 * 1024
147
+
148
+ /**
149
+ * Kitty Keyboard Protocol configuration options
150
+ * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
151
+ */
152
+ export interface KittyKeyboardOptions {
153
+ /** Disambiguate escape codes (fixes ESC timing, alt+key ambiguity, ctrl+c as event). Default: true */
154
+ disambiguate?: boolean
155
+ /** Report alternate keys (numpad, shifted, base layout) for cross-keyboard shortcuts. Default: true */
156
+ alternateKeys?: boolean
157
+ /** Report event types (press/repeat/release). Default: false */
158
+ events?: boolean
159
+ /** Report all keys as escape codes. Default: false */
160
+ allKeysAsEscapes?: boolean
161
+ /** Report text associated with key events. Default: false */
162
+ reportText?: boolean
163
+ }
164
+
165
+ /**
166
+ * Build kitty keyboard protocol flags based on configuration
167
+ * @param config Kitty keyboard configuration object (null/undefined = disabled)
168
+ * @returns The combined flags value (0 = disabled, >0 = enabled)
169
+ * @internal Exported for testing
170
+ */
171
+ export function buildKittyKeyboardFlags(config: KittyKeyboardOptions | null | undefined): number {
172
+ if (!config) {
173
+ return 0
174
+ }
175
+
176
+ let flags = 0
177
+
178
+ // Default: disambiguate + alternate keys (both default to true)
179
+ // - Disambiguate (0b1): Fixes ESC timing issues, alt+key ambiguity, makes ctrl+c a key event
180
+ // - Alternate keys (0b100): Reports shifted/base-layout keys for cross-keyboard shortcuts
181
+
182
+ // disambiguate defaults to true unless explicitly set to false
183
+ if (config.disambiguate !== false) {
184
+ flags |= KITTY_FLAG_DISAMBIGUATE
185
+ }
186
+
187
+ // alternateKeys defaults to true unless explicitly set to false
188
+ if (config.alternateKeys !== false) {
189
+ flags |= KITTY_FLAG_ALTERNATE_KEYS
190
+ }
191
+
192
+ // Optional flags (default to false, only enabled when explicitly true)
193
+ if (config.events === true) {
194
+ flags |= KITTY_FLAG_EVENT_TYPES
195
+ }
196
+
197
+ if (config.allKeysAsEscapes === true) {
198
+ flags |= KITTY_FLAG_ALL_KEYS_AS_ESCAPES
199
+ }
200
+
201
+ if (config.reportText === true) {
202
+ flags |= KITTY_FLAG_REPORT_TEXT
203
+ }
204
+
205
+ return flags
206
+ }
207
+
208
+ export class MouseEvent {
209
+ public readonly type: MouseEventType
210
+ public readonly button: number
211
+ public readonly x: number
212
+ public readonly y: number
213
+ public readonly source?: Renderable
214
+ public readonly modifiers: {
215
+ shift: boolean
216
+ alt: boolean
217
+ ctrl: boolean
218
+ }
219
+ public readonly scroll?: ScrollInfo
220
+ public readonly target: Renderable | null
221
+ public readonly isDragging?: boolean
222
+ private _propagationStopped: boolean = false
223
+ private _defaultPrevented: boolean = false
224
+
225
+ public get propagationStopped(): boolean {
226
+ return this._propagationStopped
227
+ }
228
+
229
+ public get defaultPrevented(): boolean {
230
+ return this._defaultPrevented
231
+ }
232
+
233
+ constructor(target: Renderable | null, attributes: RawMouseEvent & { source?: Renderable; isDragging?: boolean }) {
234
+ this.target = target
235
+ this.type = attributes.type
236
+ this.button = attributes.button
237
+ this.x = attributes.x
238
+ this.y = attributes.y
239
+ this.modifiers = attributes.modifiers
240
+ this.scroll = attributes.scroll
241
+ this.source = attributes.source
242
+ this.isDragging = attributes.isDragging
243
+ }
244
+
245
+ public stopPropagation(): void {
246
+ this._propagationStopped = true
247
+ }
248
+
249
+ public preventDefault(): void {
250
+ this._defaultPrevented = true
251
+ }
252
+ }
253
+
254
+ export enum MouseButton {
255
+ LEFT = 0,
256
+ MIDDLE = 1,
257
+ RIGHT = 2,
258
+ WHEEL_UP = 4,
259
+ WHEEL_DOWN = 5,
260
+ }
261
+
262
+ const rendererTracker = singleton("RendererTracker", () => {
263
+ const renderers = new Set<CliRenderer>()
264
+ return {
265
+ addRenderer: (renderer: CliRenderer) => {
266
+ renderers.add(renderer)
267
+ },
268
+ removeRenderer: (renderer: CliRenderer) => {
269
+ renderers.delete(renderer)
270
+ if (renderers.size === 0) {
271
+ process.stdin.pause()
272
+
273
+ if (hasSingleton("tree-sitter-client")) {
274
+ getTreeSitterClient().destroy()
275
+ destroySingleton("tree-sitter-client")
276
+ }
277
+ }
278
+ },
279
+ }
280
+ })
281
+
282
+ export async function createCliRenderer(config: CliRendererConfig = {}): Promise<CliRenderer> {
283
+ if (process.argv.includes("--delay-start")) {
284
+ await new Promise((resolve) => setTimeout(resolve, 5000))
285
+ }
286
+ const stdin = config.stdin || process.stdin
287
+ const stdout = config.stdout || process.stdout
288
+
289
+ const width = stdout.columns || 80
290
+ const height = stdout.rows || 24
291
+ const renderHeight =
292
+ config.experimental_splitHeight && config.experimental_splitHeight > 0 ? config.experimental_splitHeight : height
293
+
294
+ const ziglib = resolveRenderLib()
295
+ const rendererPtr = ziglib.createRenderer(width, renderHeight, {
296
+ remote: config.remote ?? false,
297
+ testing: config.testing ?? false,
298
+ })
299
+ if (!rendererPtr) {
300
+ throw new Error("Failed to create renderer")
301
+ }
302
+ if (config.useThread === undefined) {
303
+ config.useThread = true
304
+ }
305
+
306
+ // Disable threading on linux because there currently is currently an issue
307
+ // might be just a missing dependency for the build or something, but threads crash on linux
308
+ if (process.platform === "linux") {
309
+ config.useThread = false
310
+ }
311
+ ziglib.setUseThread(rendererPtr, config.useThread)
312
+
313
+ const kittyConfig = config.useKittyKeyboard ?? {}
314
+ const kittyFlags = buildKittyKeyboardFlags(kittyConfig)
315
+
316
+ ziglib.setKittyKeyboardFlags(rendererPtr, kittyFlags)
317
+
318
+ const renderer = new CliRenderer(ziglib, rendererPtr, stdin, stdout, width, height, config)
319
+ if (!config.testing) {
320
+ await renderer.setupTerminal()
321
+ }
322
+ return renderer
323
+ }
324
+
325
+ export enum CliRenderEvents {
326
+ DEBUG_OVERLAY_TOGGLE = "debugOverlay:toggle",
327
+ DESTROY = "destroy",
328
+ }
329
+
330
+ export enum RendererControlState {
331
+ IDLE = "idle",
332
+ AUTO_STARTED = "auto_started",
333
+ EXPLICIT_STARTED = "explicit_started",
334
+ EXPLICIT_PAUSED = "explicit_paused",
335
+ EXPLICIT_SUSPENDED = "explicit_suspended",
336
+ EXPLICIT_STOPPED = "explicit_stopped",
337
+ }
338
+
339
+ export class CliRenderer extends EventEmitter implements RenderContext {
340
+ private static animationFrameId = 0
341
+ private lib: RenderLib
342
+ public rendererPtr: Pointer
343
+ public stdin: NodeJS.ReadStream
344
+ private stdout: NodeJS.WriteStream
345
+ private exitOnCtrlC: boolean
346
+ private exitSignals: NodeJS.Signals[]
347
+ private _exitListenersAdded: boolean = false
348
+ private _isDestroyed: boolean = false
349
+ private _destroyPending: boolean = false
350
+ private _destroyFinalized: boolean = false
351
+ public nextRenderBuffer: OptimizedBuffer
352
+ public currentRenderBuffer: OptimizedBuffer
353
+ private _isRunning: boolean = false
354
+ private targetFps: number = 30
355
+ private maxFps: number = 60
356
+ private automaticMemorySnapshot: boolean = false
357
+ private memorySnapshotInterval: number
358
+ private memorySnapshotTimer: TimerHandle | null = null
359
+ private lastMemorySnapshot: { heapUsed: number; heapTotal: number; arrayBuffers: number } = {
360
+ heapUsed: 0,
361
+ heapTotal: 0,
362
+ arrayBuffers: 0,
363
+ }
364
+ public readonly root: RootRenderable
365
+ public width: number
366
+ public height: number
367
+ private _useThread: boolean = false
368
+ private gatherStats: boolean = false
369
+ private frameTimes: number[] = []
370
+ private maxStatSamples: number = 300
371
+ private postProcessFns: ((buffer: OptimizedBuffer, deltaTime: number) => void)[] = []
372
+ private backgroundColor: RGBA = RGBA.fromInts(0, 0, 0, 0)
373
+ private waitingForPixelResolution: boolean = false
374
+ private readonly clock: Clock
375
+
376
+ private rendering: boolean = false
377
+ private renderingNative: boolean = false
378
+ private renderTimeout: TimerHandle | null = null
379
+ private lastTime: number = 0
380
+ private frameCount: number = 0
381
+ private lastFpsTime: number = 0
382
+ private currentFps: number = 0
383
+ private targetFrameTime: number = 1000 / this.targetFps
384
+ private minTargetFrameTime: number = 1000 / this.maxFps
385
+ private immediateRerenderRequested: boolean = false
386
+ private updateScheduled: boolean = false
387
+
388
+ private liveRequestCounter: number = 0
389
+ private _controlState: RendererControlState = RendererControlState.IDLE
390
+
391
+ private frameCallbacks: ((deltaTime: number) => Promise<void>)[] = []
392
+ private renderStats: {
393
+ frameCount: number
394
+ fps: number
395
+ renderTime?: number
396
+ frameCallbackTime: number
397
+ } = {
398
+ frameCount: 0,
399
+ fps: 0,
400
+ renderTime: 0,
401
+ frameCallbackTime: 0,
402
+ }
403
+ public debugOverlay = {
404
+ enabled: env.OTUI_SHOW_STATS,
405
+ corner: DebugOverlayCorner.bottomRight,
406
+ }
407
+
408
+ private _console: TerminalConsole
409
+ private _resolution: PixelResolution | null = null
410
+ private _keyHandler: InternalKeyHandler
411
+ private stdinParser: StdinParser | null = null
412
+ private readonly oscSubscribers = new Set<(sequence: string) => void>()
413
+ private hasLoggedStdinParserError = false
414
+
415
+ private animationRequest: Map<number, FrameRequestCallback> = new Map()
416
+
417
+ private resizeTimeoutId: TimerHandle | null = null
418
+ private capabilityTimeoutId: TimerHandle | null = null
419
+ private resizeDebounceDelay: number = 100
420
+
421
+ private enableMouseMovement: boolean = false
422
+ private _useMouse: boolean = true
423
+ private autoFocus: boolean = true
424
+ private _useAlternateScreen: boolean = env.OTUI_USE_ALTERNATE_SCREEN
425
+ private _suspendedMouseEnabled: boolean = false
426
+ private _previousControlState: RendererControlState = RendererControlState.IDLE
427
+ private capturedRenderable?: Renderable
428
+ private lastOverRenderableNum: number = 0
429
+ private lastOverRenderable?: Renderable
430
+
431
+ private currentSelection: Selection | null = null
432
+ private selectionContainers: Renderable[] = []
433
+ private clipboard: Clipboard
434
+
435
+ private _splitHeight: number = 0
436
+ private renderOffset: number = 0
437
+
438
+ private _terminalWidth: number = 0
439
+ private _terminalHeight: number = 0
440
+ private _terminalIsSetup: boolean = false
441
+
442
+ private realStdoutWrite: (chunk: any, encoding?: any, callback?: any) => boolean
443
+ private captureCallback: () => void = () => {
444
+ if (this._splitHeight > 0) {
445
+ this.requestRender()
446
+ }
447
+ }
448
+
449
+ private _useConsole: boolean = true
450
+ private sigwinchHandler: () => void = (() => {
451
+ const width = this.stdout.columns || 80
452
+ const height = this.stdout.rows || 24
453
+ this.handleResize(width, height)
454
+ }).bind(this)
455
+ private _capabilities: any | null = null
456
+ private _latestPointer: { x: number; y: number } = { x: 0, y: 0 }
457
+ private _hasPointer: boolean = false
458
+ private _lastPointerModifiers: RawMouseEvent["modifiers"] = { shift: false, alt: false, ctrl: false }
459
+ private _currentMousePointerStyle: MousePointerStyle | undefined = undefined
460
+
461
+ private _currentFocusedRenderable: Renderable | null = null
462
+ private lifecyclePasses: Set<Renderable> = new Set()
463
+ private _openConsoleOnError: boolean = true
464
+ private _paletteDetector: TerminalPaletteDetector | null = null
465
+ private _cachedPalette: TerminalColors | null = null
466
+ private _paletteDetectionPromise: Promise<TerminalColors> | null = null
467
+ private _onDestroy?: () => void
468
+ private _themeMode: ThemeMode | null = null
469
+
470
+ private sequenceHandlers: ((sequence: string) => boolean)[] = []
471
+ private prependedInputHandlers: ((sequence: string) => boolean)[] = []
472
+ private shouldRestoreModesOnNextFocus: boolean = false
473
+
474
+ private idleResolvers: (() => void)[] = []
475
+
476
+ private _debugInputs: Array<{ timestamp: string; sequence: string }> = []
477
+ private _debugModeEnabled: boolean = env.OTUI_DEBUG
478
+
479
+ private handleError: (error: Error) => void = ((error: Error) => {
480
+ console.error(error)
481
+
482
+ if (this._openConsoleOnError) {
483
+ this.console.show()
484
+ }
485
+ }).bind(this)
486
+
487
+ private dumpOutputCache(optionalMessage: string = ""): void {
488
+ const cachedLogs = this.console.getCachedLogs()
489
+ const capturedOutput = capture.claimOutput()
490
+
491
+ if (capturedOutput.length > 0 || cachedLogs.length > 0) {
492
+ this.realStdoutWrite.call(this.stdout, optionalMessage)
493
+ }
494
+
495
+ if (cachedLogs.length > 0) {
496
+ this.realStdoutWrite.call(this.stdout, "Console cache:\n")
497
+ this.realStdoutWrite.call(this.stdout, cachedLogs)
498
+ }
499
+
500
+ if (capturedOutput.length > 0) {
501
+ this.realStdoutWrite.call(this.stdout, "\nCaptured output:\n")
502
+ this.realStdoutWrite.call(this.stdout, capturedOutput + "\n")
503
+ }
504
+
505
+ this.realStdoutWrite.call(this.stdout, ANSI.reset)
506
+ }
507
+
508
+ private exitHandler: () => void = (() => {
509
+ this.destroy()
510
+ if (env.OTUI_DUMP_CAPTURES) {
511
+ Bun.sleep(100).then(() => {
512
+ this.dumpOutputCache("=== CAPTURED OUTPUT ===\n")
513
+ })
514
+ }
515
+ }).bind(this)
516
+
517
+ private warningHandler: (warning: any) => void = ((warning: any) => {
518
+ console.warn(JSON.stringify(warning.message, null, 2))
519
+ }).bind(this)
520
+
521
+ public get controlState(): RendererControlState {
522
+ return this._controlState
523
+ }
524
+
525
+ constructor(
526
+ lib: RenderLib,
527
+ rendererPtr: Pointer,
528
+ stdin: NodeJS.ReadStream,
529
+ stdout: NodeJS.WriteStream,
530
+ width: number,
531
+ height: number,
532
+ config: CliRendererConfig = {},
533
+ ) {
534
+ super()
535
+
536
+ rendererTracker.addRenderer(this)
537
+
538
+ this.stdin = stdin
539
+ this.stdout = stdout
540
+ this.realStdoutWrite = stdout.write
541
+ this.lib = lib
542
+ this._terminalWidth = stdout.columns ?? width
543
+ this._terminalHeight = stdout.rows ?? height
544
+ this.width = width
545
+ this.height = height
546
+ this._useThread = config.useThread === undefined ? false : config.useThread
547
+ this._splitHeight = config.experimental_splitHeight || 0
548
+
549
+ if (this._splitHeight > 0) {
550
+ capture.on("write", this.captureCallback)
551
+ this.renderOffset = height - this._splitHeight
552
+ this.height = this._splitHeight
553
+ lib.setRenderOffset(rendererPtr, this.renderOffset)
554
+ }
555
+
556
+ this.rendererPtr = rendererPtr
557
+
558
+ const forwardEnvKeys = config.forwardEnvKeys ?? [...DEFAULT_FORWARDED_ENV_KEYS]
559
+ for (const key of forwardEnvKeys) {
560
+ const value = process.env[key]
561
+ if (value === undefined) continue
562
+ this.lib.setTerminalEnvVar(this.rendererPtr, key, value)
563
+ }
564
+
565
+ this.exitOnCtrlC = config.exitOnCtrlC === undefined ? true : config.exitOnCtrlC
566
+ this.exitSignals = config.exitSignals || [
567
+ "SIGINT", // Ctrl+C
568
+ "SIGTERM", // Termination signal
569
+ "SIGQUIT", // Ctrl+\
570
+ "SIGABRT", // Abort signal
571
+ "SIGHUP", // Hangup (terminal closed)
572
+ "SIGBREAK", // Ctrl+Break on Windows
573
+ "SIGPIPE", // Broken pipe
574
+ "SIGBUS", // Bus error
575
+ "SIGFPE", // Floating point exception
576
+ ]
577
+
578
+ this.clipboard = new Clipboard(this.lib, this.rendererPtr)
579
+ this.resizeDebounceDelay = config.debounceDelay || 100
580
+ this.targetFps = config.targetFps || 30
581
+ this.maxFps = config.maxFps || 60
582
+ this.targetFrameTime = 1000 / this.targetFps
583
+ this.minTargetFrameTime = 1000 / this.maxFps
584
+ this.memorySnapshotInterval = config.memorySnapshotInterval ?? 0
585
+ this.gatherStats = config.gatherStats || false
586
+ this.maxStatSamples = config.maxStatSamples || 300
587
+ this.enableMouseMovement = config.enableMouseMovement ?? true
588
+ this._useMouse = config.useMouse ?? true
589
+ this.autoFocus = config.autoFocus ?? true
590
+ this._useAlternateScreen = config.useAlternateScreen ?? env.OTUI_USE_ALTERNATE_SCREEN
591
+ this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)
592
+ this.currentRenderBuffer = this.lib.getCurrentBuffer(this.rendererPtr)
593
+ this.postProcessFns = config.postProcessFns || []
594
+ this.prependedInputHandlers = config.prependInputHandlers || []
595
+
596
+ this.root = new RootRenderable(this)
597
+
598
+ if (this.memorySnapshotInterval > 0) {
599
+ this.startMemorySnapshotTimer()
600
+ }
601
+
602
+ if (env.OTUI_OVERRIDE_STDOUT) {
603
+ this.stdout.write = this.interceptStdoutWrite.bind(this)
604
+ }
605
+
606
+ // Handle terminal resize
607
+ process.on("SIGWINCH", this.sigwinchHandler)
608
+
609
+ process.on("warning", this.warningHandler)
610
+
611
+ process.on("uncaughtException", this.handleError)
612
+ process.on("unhandledRejection", this.handleError)
613
+ process.on("beforeExit", this.exitHandler)
614
+
615
+ const kittyConfig = config.useKittyKeyboard ?? {}
616
+ const useKittyForParsing = kittyConfig !== null
617
+ this._keyHandler = new InternalKeyHandler()
618
+ this._keyHandler.on("keypress", (event) => {
619
+ if (this.exitOnCtrlC && event.name === "c" && event.ctrl) {
620
+ process.nextTick(() => {
621
+ this.destroy()
622
+ })
623
+ return
624
+ }
625
+ })
626
+
627
+ this.addExitListeners()
628
+
629
+ this.clock = config.clock ?? new SystemClock()
630
+
631
+ const stdinParserMaxBufferBytes = config.stdinParserMaxBufferBytes ?? DEFAULT_STDIN_PARSER_MAX_BUFFER_BYTES
632
+ this.stdinParser = new StdinParser({
633
+ timeoutMs: 10,
634
+ maxPendingBytes: stdinParserMaxBufferBytes,
635
+ armTimeouts: true,
636
+ onTimeoutFlush: () => {
637
+ this.drainStdinParser()
638
+ },
639
+ useKittyKeyboard: useKittyForParsing,
640
+ clock: this.clock,
641
+ })
642
+
643
+ this._console = new TerminalConsole(this, {
644
+ ...(config.consoleOptions ?? {}),
645
+ clock: this.clock,
646
+ })
647
+ this.useConsole = config.useConsole ?? true
648
+ this._openConsoleOnError = config.openConsoleOnError ?? process.env.NODE_ENV !== "production"
649
+ this._onDestroy = config.onDestroy
650
+
651
+ global.requestAnimationFrame = (callback: FrameRequestCallback) => {
652
+ const id = CliRenderer.animationFrameId++
653
+ this.animationRequest.set(id, callback)
654
+ this.requestLive()
655
+ return id
656
+ }
657
+ global.cancelAnimationFrame = (handle: number) => {
658
+ this.animationRequest.delete(handle)
659
+ }
660
+
661
+ const window = global.window
662
+ if (!window) {
663
+ global.window = {} as Window & typeof globalThis
664
+ }
665
+ global.window.requestAnimationFrame = requestAnimationFrame
666
+
667
+ // Prevents output from being written to the terminal, useful for debugging
668
+ if (env.OTUI_NO_NATIVE_RENDER) {
669
+ this.renderNative = () => {
670
+ if (this._splitHeight > 0) {
671
+ this.flushStdoutCache(this._splitHeight)
672
+ }
673
+ }
674
+ }
675
+
676
+ this.setupInput()
677
+ }
678
+
679
+ private addExitListeners(): void {
680
+ if (this._exitListenersAdded || this.exitSignals.length === 0) return
681
+
682
+ this.exitSignals.forEach((signal) => {
683
+ process.addListener(signal, this.exitHandler)
684
+ })
685
+
686
+ this._exitListenersAdded = true
687
+ }
688
+
689
+ private removeExitListeners(): void {
690
+ if (!this._exitListenersAdded || this.exitSignals.length === 0) return
691
+
692
+ this.exitSignals.forEach((signal) => {
693
+ process.removeListener(signal, this.exitHandler)
694
+ })
695
+
696
+ this._exitListenersAdded = false
697
+ }
698
+
699
+ public get isDestroyed(): boolean {
700
+ return this._isDestroyed
701
+ }
702
+
703
+ public registerLifecyclePass(renderable: Renderable) {
704
+ this.lifecyclePasses.add(renderable)
705
+ }
706
+
707
+ public unregisterLifecyclePass(renderable: Renderable) {
708
+ this.lifecyclePasses.delete(renderable)
709
+ }
710
+
711
+ public getLifecyclePasses() {
712
+ return this.lifecyclePasses
713
+ }
714
+
715
+ public get currentFocusedRenderable(): Renderable | null {
716
+ return this._currentFocusedRenderable
717
+ }
718
+
719
+ public focusRenderable(renderable: Renderable) {
720
+ if (this._currentFocusedRenderable === renderable) return
721
+
722
+ if (this._currentFocusedRenderable) {
723
+ this._currentFocusedRenderable.blur()
724
+ }
725
+
726
+ this._currentFocusedRenderable = renderable
727
+ }
728
+
729
+ private setCapturedRenderable(renderable: Renderable | undefined): void {
730
+ if (this.capturedRenderable === renderable) {
731
+ return
732
+ }
733
+ this.capturedRenderable = renderable
734
+ }
735
+
736
+ public addToHitGrid(x: number, y: number, width: number, height: number, id: number) {
737
+ if (id !== this.capturedRenderable?.num) {
738
+ this.lib.addToHitGrid(this.rendererPtr, x, y, width, height, id)
739
+ }
740
+ }
741
+
742
+ public pushHitGridScissorRect(x: number, y: number, width: number, height: number): void {
743
+ this.lib.hitGridPushScissorRect(this.rendererPtr, x, y, width, height)
744
+ }
745
+
746
+ public popHitGridScissorRect(): void {
747
+ this.lib.hitGridPopScissorRect(this.rendererPtr)
748
+ }
749
+
750
+ public clearHitGridScissorRects(): void {
751
+ this.lib.hitGridClearScissorRects(this.rendererPtr)
752
+ }
753
+
754
+ public get widthMethod(): WidthMethod {
755
+ const caps = this.capabilities
756
+ return caps?.unicode === "wcwidth" ? "wcwidth" : "unicode"
757
+ }
758
+
759
+ private writeOut(chunk: any, encoding?: any, callback?: any): boolean {
760
+ if (this.rendererPtr && this._useThread) {
761
+ const data = typeof chunk === "string" ? chunk : (chunk?.toString() ?? "")
762
+ this.lib.writeOut(this.rendererPtr, data)
763
+ if (typeof callback === "function") {
764
+ process.nextTick(callback)
765
+ }
766
+ return true
767
+ }
768
+
769
+ return this.realStdoutWrite.call(this.stdout, chunk, encoding, callback)
770
+ }
771
+
772
+ public requestRender() {
773
+ if (this._controlState === RendererControlState.EXPLICIT_SUSPENDED) {
774
+ return
775
+ }
776
+
777
+ if (this._isRunning) {
778
+ return
779
+ }
780
+
781
+ // NOTE: Using a frame callback that causes a re-render while already rendering
782
+ // leads to a continuous loop of renders.
783
+ if (this.rendering) {
784
+ this.immediateRerenderRequested = true
785
+ return
786
+ }
787
+
788
+ if (!this.updateScheduled && !this.renderTimeout) {
789
+ this.updateScheduled = true
790
+ const now = this.clock.now()
791
+ const elapsed = now - this.lastTime
792
+ const delay = Math.max(this.minTargetFrameTime - elapsed, 0)
793
+
794
+ if (delay === 0) {
795
+ process.nextTick(() => this.activateFrame())
796
+ return
797
+ }
798
+
799
+ this.clock.setTimeout(() => this.activateFrame(), delay)
800
+ }
801
+ }
802
+
803
+ private async activateFrame() {
804
+ await this.loop()
805
+ this.updateScheduled = false
806
+ this.resolveIdleIfNeeded()
807
+ }
808
+
809
+ public get useConsole(): boolean {
810
+ return this._useConsole
811
+ }
812
+
813
+ public set useConsole(value: boolean) {
814
+ this._useConsole = value
815
+ if (value) {
816
+ this.console.activate()
817
+ } else {
818
+ this.console.deactivate()
819
+ }
820
+ }
821
+
822
+ public get isRunning(): boolean {
823
+ return this._isRunning
824
+ }
825
+
826
+ private isIdleNow(): boolean {
827
+ return (
828
+ !this._isRunning &&
829
+ !this.rendering &&
830
+ !this.renderTimeout &&
831
+ !this.updateScheduled &&
832
+ !this.immediateRerenderRequested
833
+ )
834
+ }
835
+
836
+ private resolveIdleIfNeeded(): void {
837
+ if (!this.isIdleNow()) return
838
+ const resolvers = this.idleResolvers.splice(0)
839
+ for (const resolve of resolvers) {
840
+ resolve()
841
+ }
842
+ }
843
+
844
+ public idle(): Promise<void> {
845
+ if (this._isDestroyed) return Promise.resolve()
846
+ if (this.isIdleNow()) return Promise.resolve()
847
+ return new Promise<void>((resolve) => {
848
+ this.idleResolvers.push(resolve)
849
+ })
850
+ }
851
+
852
+ public get resolution(): PixelResolution | null {
853
+ return this._resolution
854
+ }
855
+
856
+ public get console(): TerminalConsole {
857
+ return this._console
858
+ }
859
+
860
+ public get keyInput(): KeyHandler {
861
+ return this._keyHandler
862
+ }
863
+
864
+ public get _internalKeyInput(): InternalKeyHandler {
865
+ return this._keyHandler
866
+ }
867
+
868
+ public get terminalWidth(): number {
869
+ return this._terminalWidth
870
+ }
871
+
872
+ public get terminalHeight(): number {
873
+ return this._terminalHeight
874
+ }
875
+
876
+ public get useThread(): boolean {
877
+ return this._useThread
878
+ }
879
+
880
+ public get useMouse(): boolean {
881
+ return this._useMouse
882
+ }
883
+
884
+ public set useMouse(useMouse: boolean) {
885
+ if (this._useMouse === useMouse) return // No change needed
886
+
887
+ this._useMouse = useMouse
888
+
889
+ if (useMouse) {
890
+ this.enableMouse()
891
+ } else {
892
+ this.disableMouse()
893
+ }
894
+ }
895
+
896
+ public get experimental_splitHeight(): number {
897
+ return this._splitHeight
898
+ }
899
+
900
+ public get liveRequestCount(): number {
901
+ return this.liveRequestCounter
902
+ }
903
+
904
+ public get currentControlState(): string {
905
+ return this._controlState
906
+ }
907
+
908
+ public get capabilities(): any | null {
909
+ return this._capabilities
910
+ }
911
+
912
+ public get themeMode(): ThemeMode | null {
913
+ return this._themeMode
914
+ }
915
+
916
+ public getDebugInputs(): Array<{ timestamp: string; sequence: string }> {
917
+ return [...this._debugInputs]
918
+ }
919
+
920
+ public get useKittyKeyboard(): boolean {
921
+ return this.lib.getKittyKeyboardFlags(this.rendererPtr) > 0
922
+ }
923
+
924
+ public set useKittyKeyboard(use: boolean) {
925
+ const flags = use ? KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS : 0
926
+ this.lib.setKittyKeyboardFlags(this.rendererPtr, flags)
927
+ }
928
+
929
+ public set experimental_splitHeight(splitHeight: number) {
930
+ if (splitHeight < 0) splitHeight = 0
931
+
932
+ const prevSplitHeight = this._splitHeight
933
+
934
+ if (splitHeight > 0) {
935
+ this._splitHeight = splitHeight
936
+ this.renderOffset = this._terminalHeight - this._splitHeight
937
+ this.height = this._splitHeight
938
+
939
+ if (prevSplitHeight === 0) {
940
+ this.useConsole = false
941
+ capture.on("write", this.captureCallback)
942
+ const freedLines = this._terminalHeight - this._splitHeight
943
+ const scrollDown = ANSI.scrollDown(freedLines)
944
+ this.writeOut(scrollDown)
945
+ } else if (prevSplitHeight > this._splitHeight) {
946
+ const freedLines = prevSplitHeight - this._splitHeight
947
+ const scrollDown = ANSI.scrollDown(freedLines)
948
+ this.writeOut(scrollDown)
949
+ } else if (prevSplitHeight < this._splitHeight) {
950
+ const additionalLines = this._splitHeight - prevSplitHeight
951
+ const scrollUp = ANSI.scrollUp(additionalLines)
952
+ this.writeOut(scrollUp)
953
+ }
954
+ } else {
955
+ if (prevSplitHeight > 0) {
956
+ this.flushStdoutCache(this._terminalHeight, true)
957
+
958
+ capture.off("write", this.captureCallback)
959
+ this.useConsole = true
960
+ }
961
+
962
+ this._splitHeight = 0
963
+ this.renderOffset = 0
964
+ this.height = this._terminalHeight
965
+ }
966
+
967
+ this.width = this._terminalWidth
968
+ this.lib.setRenderOffset(this.rendererPtr, this.renderOffset)
969
+ this.lib.resizeRenderer(this.rendererPtr, this.width, this.height)
970
+ this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)
971
+
972
+ this._console.resize(this.width, this.height)
973
+ this.root.resize(this.width, this.height)
974
+ this.emit("resize", this.width, this.height)
975
+ this.requestRender()
976
+ }
977
+
978
+ private interceptStdoutWrite = (chunk: any, encoding?: any, callback?: any): boolean => {
979
+ const text = chunk.toString()
980
+
981
+ capture.write("stdout", text)
982
+ if (this._splitHeight > 0) {
983
+ this.requestRender()
984
+ }
985
+
986
+ if (typeof callback === "function") {
987
+ process.nextTick(callback)
988
+ }
989
+
990
+ return true
991
+ }
992
+
993
+ public disableStdoutInterception(): void {
994
+ this.stdout.write = this.realStdoutWrite
995
+ }
996
+
997
+ // TODO: Move this to native
998
+ private flushStdoutCache(space: number, force: boolean = false): boolean {
999
+ if (capture.size === 0 && !force) return false
1000
+
1001
+ const output = capture.claimOutput()
1002
+
1003
+ const rendererStartLine = this._terminalHeight - this._splitHeight
1004
+ const flush = ANSI.moveCursorAndClear(rendererStartLine, 1)
1005
+
1006
+ const outputLine = this._terminalHeight - this._splitHeight
1007
+ const move = ANSI.moveCursor(outputLine, 1)
1008
+
1009
+ let clear = ""
1010
+ if (space > 0) {
1011
+ const backgroundColor = this.backgroundColor.toInts()
1012
+ const newlines = " ".repeat(this.width) + "\n".repeat(space)
1013
+ // Check if background is transparent (alpha = 0)
1014
+ if (backgroundColor[3] === 0) {
1015
+ clear = newlines
1016
+ } else {
1017
+ clear =
1018
+ ANSI.setRgbBackground(backgroundColor[0], backgroundColor[1], backgroundColor[2]) +
1019
+ newlines +
1020
+ ANSI.resetBackground
1021
+ }
1022
+ }
1023
+
1024
+ this.writeOut(flush + move + output + clear)
1025
+
1026
+ return true
1027
+ }
1028
+
1029
+ private enableMouse(): void {
1030
+ this._useMouse = true
1031
+ this.lib.enableMouse(this.rendererPtr, this.enableMouseMovement)
1032
+ }
1033
+
1034
+ private disableMouse(): void {
1035
+ this._useMouse = false
1036
+ this.setCapturedRenderable(undefined)
1037
+ this.stdinParser?.resetMouseState()
1038
+ this.lib.disableMouse(this.rendererPtr)
1039
+ }
1040
+
1041
+ public enableKittyKeyboard(flags: number = 0b00011): void {
1042
+ this.lib.enableKittyKeyboard(this.rendererPtr, flags)
1043
+ }
1044
+
1045
+ public disableKittyKeyboard(): void {
1046
+ this.lib.disableKittyKeyboard(this.rendererPtr)
1047
+ }
1048
+
1049
+ public set useThread(useThread: boolean) {
1050
+ this._useThread = useThread
1051
+ this.lib.setUseThread(this.rendererPtr, useThread)
1052
+ }
1053
+
1054
+ // TODO: All input management may move to native when zig finally has async io support again,
1055
+ // without rolling a full event loop
1056
+ public async setupTerminal(): Promise<void> {
1057
+ if (this._terminalIsSetup) return
1058
+ this._terminalIsSetup = true
1059
+
1060
+ this.lib.setupTerminal(this.rendererPtr, this._useAlternateScreen)
1061
+ this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr)
1062
+
1063
+ if (this.debugOverlay.enabled) {
1064
+ this.lib.setDebugOverlay(this.rendererPtr, true, this.debugOverlay.corner)
1065
+ if (!this.memorySnapshotInterval) {
1066
+ this.memorySnapshotInterval = 3000
1067
+ this.startMemorySnapshotTimer()
1068
+ this.automaticMemorySnapshot = true
1069
+ }
1070
+ }
1071
+
1072
+ this.capabilityTimeoutId = this.clock.setTimeout(() => {
1073
+ this.capabilityTimeoutId = null
1074
+ this.removeInputHandler(this.capabilityHandler)
1075
+ }, 5000)
1076
+
1077
+ if (this._useMouse) {
1078
+ this.enableMouse()
1079
+ }
1080
+
1081
+ this.queryPixelResolution()
1082
+ }
1083
+
1084
+ private stdinListener: (chunk: Buffer | string) => void = ((chunk: Buffer | string) => {
1085
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
1086
+ if (!this.stdinParser) return
1087
+
1088
+ try {
1089
+ this.stdinParser.push(data)
1090
+ this.drainStdinParser()
1091
+ } catch (error) {
1092
+ this.handleStdinParserFailure(error)
1093
+ }
1094
+ }).bind(this)
1095
+
1096
+ public addInputHandler(handler: (sequence: string) => boolean): void {
1097
+ this.sequenceHandlers.push(handler)
1098
+ }
1099
+
1100
+ public prependInputHandler(handler: (sequence: string) => boolean): void {
1101
+ this.sequenceHandlers.unshift(handler)
1102
+ }
1103
+
1104
+ public removeInputHandler(handler: (sequence: string) => boolean): void {
1105
+ this.sequenceHandlers = this.sequenceHandlers.filter((candidate) => candidate !== handler)
1106
+ }
1107
+
1108
+ public subscribeOsc(handler: (sequence: string) => void): () => void {
1109
+ this.oscSubscribers.add(handler)
1110
+ return () => {
1111
+ this.oscSubscribers.delete(handler)
1112
+ }
1113
+ }
1114
+
1115
+ private capabilityHandler: (sequence: string) => boolean = ((sequence: string) => {
1116
+ if (isCapabilityResponse(sequence)) {
1117
+ this.lib.processCapabilityResponse(this.rendererPtr, sequence)
1118
+ this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr)
1119
+ this.emit("capabilities", this._capabilities)
1120
+ return true
1121
+ }
1122
+ return false
1123
+ }).bind(this)
1124
+
1125
+ private focusHandler: (sequence: string) => boolean = ((sequence: string) => {
1126
+ if (sequence === "\x1b[I") {
1127
+ // When the terminal regains focus, some terminal emulators (notably
1128
+ // Windows Terminal / ConPTY) may have stripped DEC private modes like
1129
+ // mouse tracking, bracketed paste, and focus tracking itself while the
1130
+ // window was unfocused.
1131
+ if (this.shouldRestoreModesOnNextFocus) {
1132
+ this.lib.restoreTerminalModes(this.rendererPtr)
1133
+ this.shouldRestoreModesOnNextFocus = false
1134
+ }
1135
+ this.emit("focus")
1136
+ return true
1137
+ }
1138
+ if (sequence === "\x1b[O") {
1139
+ this.shouldRestoreModesOnNextFocus = true
1140
+ this.emit("blur")
1141
+ return true
1142
+ }
1143
+ return false
1144
+ }).bind(this)
1145
+
1146
+ private themeModeHandler: (sequence: string) => boolean = ((sequence: string) => {
1147
+ if (sequence === "\x1b[?997;1n") {
1148
+ if (this._themeMode !== "dark") {
1149
+ this._themeMode = "dark"
1150
+ this.emit("theme_mode", "dark")
1151
+ }
1152
+ return true
1153
+ }
1154
+ if (sequence === "\x1b[?997;2n") {
1155
+ if (this._themeMode !== "light") {
1156
+ this._themeMode = "light"
1157
+ this.emit("theme_mode", "light")
1158
+ }
1159
+ return true
1160
+ }
1161
+ return false
1162
+ }).bind(this)
1163
+
1164
+ private dispatchSequenceHandlers(sequence: string): boolean {
1165
+ if (this._debugModeEnabled) {
1166
+ this._debugInputs.push({
1167
+ timestamp: new Date().toISOString(),
1168
+ sequence,
1169
+ })
1170
+ }
1171
+
1172
+ for (const handler of this.sequenceHandlers) {
1173
+ if (handler(sequence)) {
1174
+ return true
1175
+ }
1176
+ }
1177
+
1178
+ return false
1179
+ }
1180
+
1181
+ private drainStdinParser(): void {
1182
+ if (!this.stdinParser) return
1183
+
1184
+ this.stdinParser.drain((event) => {
1185
+ this.handleStdinEvent(event)
1186
+ })
1187
+ }
1188
+
1189
+ private handleStdinEvent(event: StdinEvent): void {
1190
+ switch (event.type) {
1191
+ case "key":
1192
+ if (this.dispatchSequenceHandlers(event.raw)) {
1193
+ return
1194
+ }
1195
+
1196
+ this._keyHandler.processParsedKey(event.key)
1197
+ return
1198
+ case "mouse":
1199
+ if (this._useMouse && this.processSingleMouseEvent(event.event)) {
1200
+ return
1201
+ }
1202
+
1203
+ this.dispatchSequenceHandlers(event.raw)
1204
+ return
1205
+ case "paste":
1206
+ this._keyHandler.processPaste(event.bytes, event.metadata)
1207
+ return
1208
+ case "response":
1209
+ if (event.protocol === "osc") {
1210
+ for (const subscriber of this.oscSubscribers) {
1211
+ subscriber(event.sequence)
1212
+ }
1213
+ }
1214
+
1215
+ this.dispatchSequenceHandlers(event.sequence)
1216
+ return
1217
+ }
1218
+ }
1219
+
1220
+ private handleStdinParserFailure(error: unknown): void {
1221
+ if (!this.hasLoggedStdinParserError) {
1222
+ this.hasLoggedStdinParserError = true
1223
+ if (process.env.NODE_ENV !== "test") {
1224
+ console.error("[stdin-parser-error] parser failure, resetting parser", error)
1225
+ }
1226
+ }
1227
+
1228
+ try {
1229
+ this.stdinParser?.reset()
1230
+ } catch (resetError) {
1231
+ console.error("stdin parser reset failed after parser error", resetError)
1232
+ }
1233
+ }
1234
+
1235
+ private setupInput(): void {
1236
+ for (const handler of this.prependedInputHandlers) {
1237
+ this.addInputHandler(handler)
1238
+ }
1239
+
1240
+ this.addInputHandler((sequence: string) => {
1241
+ if (isPixelResolutionResponse(sequence) && this.waitingForPixelResolution) {
1242
+ const resolution = parsePixelResolution(sequence)
1243
+ if (resolution) {
1244
+ this._resolution = resolution
1245
+ this.waitingForPixelResolution = false
1246
+ }
1247
+ return true
1248
+ }
1249
+ return false
1250
+ })
1251
+ this.addInputHandler(this.capabilityHandler)
1252
+ this.addInputHandler(this.focusHandler)
1253
+ this.addInputHandler(this.themeModeHandler)
1254
+
1255
+ if (this.stdin.setRawMode) {
1256
+ this.stdin.setRawMode(true)
1257
+ }
1258
+
1259
+ this.stdin.resume()
1260
+ this.stdin.on("data", this.stdinListener)
1261
+ }
1262
+
1263
+ private dispatchMouseEvent(
1264
+ target: Renderable,
1265
+ attributes: RawMouseEvent & { source?: Renderable; isDragging?: boolean },
1266
+ ): MouseEvent {
1267
+ const event = new MouseEvent(target, attributes)
1268
+ target.processMouseEvent(event)
1269
+
1270
+ if (this.autoFocus && event.type === "down" && event.button === MouseButton.LEFT && !event.defaultPrevented) {
1271
+ let current: Renderable | null = target
1272
+ while (current) {
1273
+ if (current.focusable) {
1274
+ current.focus()
1275
+ break
1276
+ }
1277
+ current = current.parent
1278
+ }
1279
+ }
1280
+
1281
+ return event
1282
+ }
1283
+
1284
+ private processSingleMouseEvent(mouseEvent: RawMouseEvent): boolean {
1285
+ if (this._splitHeight > 0) {
1286
+ if (mouseEvent.y < this.renderOffset) {
1287
+ return false
1288
+ }
1289
+ mouseEvent.y -= this.renderOffset
1290
+ }
1291
+
1292
+ this._latestPointer.x = mouseEvent.x
1293
+ this._latestPointer.y = mouseEvent.y
1294
+ this._hasPointer = true
1295
+ this._lastPointerModifiers = mouseEvent.modifiers
1296
+
1297
+ if (this._console.visible) {
1298
+ const consoleBounds = this._console.bounds
1299
+ if (
1300
+ mouseEvent.x >= consoleBounds.x &&
1301
+ mouseEvent.x < consoleBounds.x + consoleBounds.width &&
1302
+ mouseEvent.y >= consoleBounds.y &&
1303
+ mouseEvent.y < consoleBounds.y + consoleBounds.height
1304
+ ) {
1305
+ const event = new MouseEvent(null, mouseEvent)
1306
+ const handled = this._console.handleMouse(event)
1307
+ if (handled) return true
1308
+ }
1309
+ }
1310
+
1311
+ if (mouseEvent.type === "scroll") {
1312
+ const maybeRenderableId = this.hitTest(mouseEvent.x, mouseEvent.y)
1313
+ const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)
1314
+ const fallbackTarget =
1315
+ this._currentFocusedRenderable &&
1316
+ !this._currentFocusedRenderable.isDestroyed &&
1317
+ this._currentFocusedRenderable.focused
1318
+ ? this._currentFocusedRenderable
1319
+ : null
1320
+ const scrollTarget = maybeRenderable ?? fallbackTarget
1321
+
1322
+ if (scrollTarget) {
1323
+ const event = new MouseEvent(scrollTarget, mouseEvent)
1324
+ scrollTarget.processMouseEvent(event)
1325
+ }
1326
+ return true
1327
+ }
1328
+
1329
+ const maybeRenderableId = this.hitTest(mouseEvent.x, mouseEvent.y)
1330
+ const sameElement = maybeRenderableId === this.lastOverRenderableNum
1331
+ this.lastOverRenderableNum = maybeRenderableId
1332
+ const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)
1333
+
1334
+ if (
1335
+ mouseEvent.type === "down" &&
1336
+ mouseEvent.button === MouseButton.LEFT &&
1337
+ !this.currentSelection?.isDragging &&
1338
+ !mouseEvent.modifiers.ctrl
1339
+ ) {
1340
+ const canStartSelection = Boolean(
1341
+ maybeRenderable &&
1342
+ maybeRenderable.selectable &&
1343
+ !maybeRenderable.isDestroyed &&
1344
+ maybeRenderable.shouldStartSelection(mouseEvent.x, mouseEvent.y),
1345
+ )
1346
+
1347
+ if (canStartSelection && maybeRenderable) {
1348
+ this.startSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)
1349
+ this.dispatchMouseEvent(maybeRenderable, mouseEvent)
1350
+ return true
1351
+ }
1352
+ }
1353
+
1354
+ if (mouseEvent.type === "drag" && this.currentSelection?.isDragging) {
1355
+ this.updateSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)
1356
+
1357
+ if (maybeRenderable) {
1358
+ const event = new MouseEvent(maybeRenderable, { ...mouseEvent, isDragging: true })
1359
+ maybeRenderable.processMouseEvent(event)
1360
+ }
1361
+
1362
+ return true
1363
+ }
1364
+
1365
+ if (mouseEvent.type === "up" && this.currentSelection?.isDragging) {
1366
+ if (maybeRenderable) {
1367
+ const event = new MouseEvent(maybeRenderable, { ...mouseEvent, isDragging: true })
1368
+ maybeRenderable.processMouseEvent(event)
1369
+ }
1370
+
1371
+ this.finishSelection()
1372
+ return true
1373
+ }
1374
+
1375
+ if (mouseEvent.type === "down" && mouseEvent.button === MouseButton.LEFT && this.currentSelection) {
1376
+ if (mouseEvent.modifiers.ctrl) {
1377
+ this.currentSelection.isDragging = true
1378
+ this.updateSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)
1379
+ return true
1380
+ }
1381
+ }
1382
+
1383
+ if (!sameElement && (mouseEvent.type === "drag" || mouseEvent.type === "move")) {
1384
+ if (
1385
+ this.lastOverRenderable &&
1386
+ this.lastOverRenderable !== this.capturedRenderable &&
1387
+ !this.lastOverRenderable.isDestroyed
1388
+ ) {
1389
+ const event = new MouseEvent(this.lastOverRenderable, { ...mouseEvent, type: "out" })
1390
+ this.lastOverRenderable.processMouseEvent(event)
1391
+ }
1392
+ this.lastOverRenderable = maybeRenderable
1393
+ if (maybeRenderable) {
1394
+ const event = new MouseEvent(maybeRenderable, {
1395
+ ...mouseEvent,
1396
+ type: "over",
1397
+ source: this.capturedRenderable,
1398
+ })
1399
+ maybeRenderable.processMouseEvent(event)
1400
+ }
1401
+ }
1402
+
1403
+ if (this.capturedRenderable && mouseEvent.type !== "up") {
1404
+ const event = new MouseEvent(this.capturedRenderable, mouseEvent)
1405
+ this.capturedRenderable.processMouseEvent(event)
1406
+ return true
1407
+ }
1408
+
1409
+ if (this.capturedRenderable && mouseEvent.type === "up") {
1410
+ const event = new MouseEvent(this.capturedRenderable, { ...mouseEvent, type: "drag-end" })
1411
+ this.capturedRenderable.processMouseEvent(event)
1412
+ this.capturedRenderable.processMouseEvent(new MouseEvent(this.capturedRenderable, mouseEvent))
1413
+ if (maybeRenderable) {
1414
+ const event = new MouseEvent(maybeRenderable, {
1415
+ ...mouseEvent,
1416
+ type: "drop",
1417
+ source: this.capturedRenderable,
1418
+ })
1419
+ maybeRenderable.processMouseEvent(event)
1420
+ }
1421
+ this.lastOverRenderable = this.capturedRenderable
1422
+ this.lastOverRenderableNum = this.capturedRenderable.num
1423
+ this.setCapturedRenderable(undefined)
1424
+ // Dropping the renderable needs to push another frame when the renderer is not live
1425
+ // to update the hit grid, otherwise capturedRenderable won't be in the hit grid and will not receive mouse events
1426
+ this.requestRender()
1427
+ }
1428
+
1429
+ let event: MouseEvent | undefined
1430
+ if (maybeRenderable) {
1431
+ if (mouseEvent.type === "drag" && mouseEvent.button === MouseButton.LEFT) {
1432
+ this.setCapturedRenderable(maybeRenderable)
1433
+ } else {
1434
+ this.setCapturedRenderable(undefined)
1435
+ }
1436
+ event = this.dispatchMouseEvent(maybeRenderable, mouseEvent)
1437
+ } else {
1438
+ this.setCapturedRenderable(undefined)
1439
+ this.lastOverRenderable = undefined
1440
+ }
1441
+
1442
+ if (!event?.defaultPrevented && mouseEvent.type === "down" && this.currentSelection) {
1443
+ this.clearSelection()
1444
+ }
1445
+
1446
+ return true
1447
+ }
1448
+
1449
+ /**
1450
+ * Recheck hover state after hit grid changes.
1451
+ * Called after render when native code detects the hit grid changed.
1452
+ * Fires out/over events if the element under the cursor changed.
1453
+ */
1454
+ private recheckHoverState(): void {
1455
+ if (this._isDestroyed || !this._hasPointer) return
1456
+ if (this.capturedRenderable) return
1457
+
1458
+ const hitId = this.hitTest(this._latestPointer.x, this._latestPointer.y)
1459
+ const hitRenderable = Renderable.renderablesByNumber.get(hitId)
1460
+ const lastOver = this.lastOverRenderable
1461
+
1462
+ // No change
1463
+ if (lastOver?.num === hitId) {
1464
+ this.lastOverRenderableNum = hitId
1465
+ return
1466
+ }
1467
+
1468
+ const baseEvent: RawMouseEvent = {
1469
+ type: "move",
1470
+ button: 0,
1471
+ x: this._latestPointer.x,
1472
+ y: this._latestPointer.y,
1473
+ modifiers: this._lastPointerModifiers,
1474
+ }
1475
+
1476
+ // Fire out on old element
1477
+ if (lastOver && !lastOver.isDestroyed) {
1478
+ const event = new MouseEvent(lastOver, { ...baseEvent, type: "out" })
1479
+ lastOver.processMouseEvent(event)
1480
+ }
1481
+
1482
+ this.lastOverRenderable = hitRenderable
1483
+ this.lastOverRenderableNum = hitId
1484
+
1485
+ // Fire over on new element
1486
+ if (hitRenderable) {
1487
+ const event = new MouseEvent(hitRenderable, {
1488
+ ...baseEvent,
1489
+ type: "over",
1490
+ })
1491
+ hitRenderable.processMouseEvent(event)
1492
+ }
1493
+ }
1494
+ public setMousePointer(style: MousePointerStyle): void {
1495
+ this._currentMousePointerStyle = style
1496
+ this.lib.setCursorStyleOptions(this.rendererPtr, { cursor: style })
1497
+ }
1498
+
1499
+ public hitTest(x: number, y: number): number {
1500
+ return this.lib.checkHit(this.rendererPtr, x, y)
1501
+ }
1502
+
1503
+ private takeMemorySnapshot(): void {
1504
+ if (this._isDestroyed) return
1505
+
1506
+ const memoryUsage = process.memoryUsage()
1507
+ this.lastMemorySnapshot = {
1508
+ heapUsed: memoryUsage.heapUsed,
1509
+ heapTotal: memoryUsage.heapTotal,
1510
+ arrayBuffers: memoryUsage.arrayBuffers,
1511
+ }
1512
+
1513
+ this.lib.updateMemoryStats(
1514
+ this.rendererPtr,
1515
+ this.lastMemorySnapshot.heapUsed,
1516
+ this.lastMemorySnapshot.heapTotal,
1517
+ this.lastMemorySnapshot.arrayBuffers,
1518
+ )
1519
+
1520
+ this.emit("memory:snapshot", this.lastMemorySnapshot)
1521
+ }
1522
+
1523
+ private startMemorySnapshotTimer(): void {
1524
+ this.stopMemorySnapshotTimer()
1525
+
1526
+ this.memorySnapshotTimer = this.clock.setInterval(() => {
1527
+ this.takeMemorySnapshot()
1528
+ }, this.memorySnapshotInterval)
1529
+ }
1530
+
1531
+ private stopMemorySnapshotTimer(): void {
1532
+ if (this.memorySnapshotTimer) {
1533
+ this.clock.clearInterval(this.memorySnapshotTimer)
1534
+ this.memorySnapshotTimer = null
1535
+ }
1536
+ }
1537
+
1538
+ public setMemorySnapshotInterval(interval: number): void {
1539
+ this.memorySnapshotInterval = interval
1540
+
1541
+ if (this._isRunning && interval > 0) {
1542
+ this.startMemorySnapshotTimer()
1543
+ } else if (interval <= 0 && this.memorySnapshotTimer) {
1544
+ this.clock.clearInterval(this.memorySnapshotTimer)
1545
+ this.memorySnapshotTimer = null
1546
+ }
1547
+ }
1548
+
1549
+ private handleResize(width: number, height: number): void {
1550
+ if (this._isDestroyed) return
1551
+ if (this._splitHeight > 0) {
1552
+ this.processResize(width, height)
1553
+ return
1554
+ }
1555
+
1556
+ if (this.resizeTimeoutId !== null) {
1557
+ this.clock.clearTimeout(this.resizeTimeoutId)
1558
+ this.resizeTimeoutId = null
1559
+ }
1560
+
1561
+ this.resizeTimeoutId = this.clock.setTimeout(() => {
1562
+ this.resizeTimeoutId = null
1563
+ this.processResize(width, height)
1564
+ }, this.resizeDebounceDelay)
1565
+ }
1566
+
1567
+ private queryPixelResolution() {
1568
+ this.waitingForPixelResolution = true
1569
+ this.lib.queryPixelResolution(this.rendererPtr)
1570
+ }
1571
+
1572
+ private processResize(width: number, height: number): void {
1573
+ if (width === this._terminalWidth && height === this._terminalHeight) return
1574
+
1575
+ const prevWidth = this._terminalWidth
1576
+
1577
+ this._terminalWidth = width
1578
+ this._terminalHeight = height
1579
+ this.queryPixelResolution()
1580
+
1581
+ this.setCapturedRenderable(undefined)
1582
+ this.stdinParser?.resetMouseState()
1583
+
1584
+ if (this._splitHeight > 0) {
1585
+ // TODO: Handle resizing split mode properly
1586
+ if (width < prevWidth) {
1587
+ const start = this._terminalHeight - this._splitHeight * 2
1588
+ const flush = ANSI.moveCursorAndClear(start, 1)
1589
+ this.writeOut(flush)
1590
+ }
1591
+ this.renderOffset = height - this._splitHeight
1592
+ this.width = width
1593
+ this.height = this._splitHeight
1594
+ this.currentRenderBuffer.clear(this.backgroundColor)
1595
+ this.lib.setRenderOffset(this.rendererPtr, this.renderOffset)
1596
+ } else {
1597
+ this.width = width
1598
+ this.height = height
1599
+ }
1600
+
1601
+ this.lib.resizeRenderer(this.rendererPtr, this.width, this.height)
1602
+ this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)
1603
+ this.currentRenderBuffer = this.lib.getCurrentBuffer(this.rendererPtr)
1604
+ this._console.resize(this.width, this.height)
1605
+ this.root.resize(this.width, this.height)
1606
+ this.emit("resize", this.width, this.height)
1607
+ this.requestRender()
1608
+ }
1609
+
1610
+ public setBackgroundColor(color: ColorInput): void {
1611
+ const parsedColor = parseColor(color)
1612
+ this.lib.setBackgroundColor(this.rendererPtr, parsedColor as RGBA)
1613
+ this.backgroundColor = parsedColor as RGBA
1614
+ this.nextRenderBuffer.clear(parsedColor as RGBA)
1615
+ this.requestRender()
1616
+ }
1617
+
1618
+ public toggleDebugOverlay(): void {
1619
+ const willBeEnabled = !this.debugOverlay.enabled
1620
+
1621
+ if (willBeEnabled && !this.memorySnapshotInterval) {
1622
+ this.memorySnapshotInterval = 3000
1623
+ this.startMemorySnapshotTimer()
1624
+ this.automaticMemorySnapshot = true
1625
+ } else if (!willBeEnabled && this.automaticMemorySnapshot) {
1626
+ this.stopMemorySnapshotTimer()
1627
+ this.memorySnapshotInterval = 0
1628
+ this.automaticMemorySnapshot = false
1629
+ }
1630
+
1631
+ this.debugOverlay.enabled = !this.debugOverlay.enabled
1632
+ this.lib.setDebugOverlay(this.rendererPtr, this.debugOverlay.enabled, this.debugOverlay.corner)
1633
+ this.emit(CliRenderEvents.DEBUG_OVERLAY_TOGGLE, this.debugOverlay.enabled)
1634
+ this.requestRender()
1635
+ }
1636
+
1637
+ public configureDebugOverlay(options: { enabled?: boolean; corner?: DebugOverlayCorner }): void {
1638
+ this.debugOverlay.enabled = options.enabled ?? this.debugOverlay.enabled
1639
+ this.debugOverlay.corner = options.corner ?? this.debugOverlay.corner
1640
+ this.lib.setDebugOverlay(this.rendererPtr, this.debugOverlay.enabled, this.debugOverlay.corner)
1641
+ this.requestRender()
1642
+ }
1643
+
1644
+ public setTerminalTitle(title: string): void {
1645
+ this.lib.setTerminalTitle(this.rendererPtr, title)
1646
+ }
1647
+
1648
+ public copyToClipboardOSC52(text: string, target?: ClipboardTarget): boolean {
1649
+ return this.clipboard.copyToClipboardOSC52(text, target)
1650
+ }
1651
+
1652
+ public clearClipboardOSC52(target?: ClipboardTarget): boolean {
1653
+ return this.clipboard.clearClipboardOSC52(target)
1654
+ }
1655
+
1656
+ public isOsc52Supported(): boolean {
1657
+ return this._capabilities?.osc52 ?? this.clipboard.isOsc52Supported()
1658
+ }
1659
+
1660
+ public dumpHitGrid(): void {
1661
+ this.lib.dumpHitGrid(this.rendererPtr)
1662
+ }
1663
+
1664
+ public dumpBuffers(timestamp?: number): void {
1665
+ this.lib.dumpBuffers(this.rendererPtr, timestamp)
1666
+ }
1667
+
1668
+ public dumpStdoutBuffer(timestamp?: number): void {
1669
+ this.lib.dumpStdoutBuffer(this.rendererPtr, timestamp)
1670
+ }
1671
+
1672
+ public static setCursorPosition(renderer: CliRenderer, x: number, y: number, visible: boolean = true): void {
1673
+ const lib = resolveRenderLib()
1674
+ lib.setCursorPosition(renderer.rendererPtr, x, y, visible)
1675
+ }
1676
+
1677
+ public static setCursorStyle(renderer: CliRenderer, options: CursorStyleOptions): void {
1678
+ const lib = resolveRenderLib()
1679
+ lib.setCursorStyleOptions(renderer.rendererPtr, options)
1680
+ if (options.cursor !== undefined) {
1681
+ renderer._currentMousePointerStyle = options.cursor
1682
+ }
1683
+ }
1684
+
1685
+ public static setCursorColor(renderer: CliRenderer, color: RGBA): void {
1686
+ const lib = resolveRenderLib()
1687
+ lib.setCursorColor(renderer.rendererPtr, color)
1688
+ }
1689
+
1690
+ public setCursorPosition(x: number, y: number, visible: boolean = true): void {
1691
+ this.lib.setCursorPosition(this.rendererPtr, x, y, visible)
1692
+ }
1693
+
1694
+ public setCursorStyle(options: CursorStyleOptions): void {
1695
+ this.lib.setCursorStyleOptions(this.rendererPtr, options)
1696
+ if (options.cursor !== undefined) {
1697
+ this._currentMousePointerStyle = options.cursor
1698
+ }
1699
+ }
1700
+
1701
+ public setCursorColor(color: RGBA): void {
1702
+ this.lib.setCursorColor(this.rendererPtr, color)
1703
+ }
1704
+
1705
+ public getCursorState() {
1706
+ return this.lib.getCursorState(this.rendererPtr)
1707
+ }
1708
+
1709
+ public addPostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void {
1710
+ this.postProcessFns.push(processFn)
1711
+ }
1712
+
1713
+ public removePostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void {
1714
+ this.postProcessFns = this.postProcessFns.filter((fn) => fn !== processFn)
1715
+ }
1716
+
1717
+ public clearPostProcessFns(): void {
1718
+ this.postProcessFns = []
1719
+ }
1720
+
1721
+ public setFrameCallback(callback: (deltaTime: number) => Promise<void>): void {
1722
+ this.frameCallbacks.push(callback)
1723
+ }
1724
+
1725
+ public removeFrameCallback(callback: (deltaTime: number) => Promise<void>): void {
1726
+ this.frameCallbacks = this.frameCallbacks.filter((cb) => cb !== callback)
1727
+ }
1728
+
1729
+ public clearFrameCallbacks(): void {
1730
+ this.frameCallbacks = []
1731
+ }
1732
+
1733
+ public requestLive(): void {
1734
+ this.liveRequestCounter++
1735
+
1736
+ if (this._controlState === RendererControlState.IDLE && this.liveRequestCounter > 0) {
1737
+ this._controlState = RendererControlState.AUTO_STARTED
1738
+ this.internalStart()
1739
+ }
1740
+ }
1741
+
1742
+ public dropLive(): void {
1743
+ this.liveRequestCounter = Math.max(0, this.liveRequestCounter - 1)
1744
+
1745
+ if (this._controlState === RendererControlState.AUTO_STARTED && this.liveRequestCounter === 0) {
1746
+ this._controlState = RendererControlState.IDLE
1747
+ this.internalPause()
1748
+ }
1749
+ }
1750
+
1751
+ public start(): void {
1752
+ this._controlState = RendererControlState.EXPLICIT_STARTED
1753
+ this.internalStart()
1754
+ }
1755
+
1756
+ public auto(): void {
1757
+ this._controlState = this._isRunning ? RendererControlState.AUTO_STARTED : RendererControlState.IDLE
1758
+ }
1759
+
1760
+ private internalStart(): void {
1761
+ if (!this._isRunning && !this._isDestroyed) {
1762
+ this._isRunning = true
1763
+
1764
+ if (this.memorySnapshotInterval > 0) {
1765
+ this.startMemorySnapshotTimer()
1766
+ }
1767
+
1768
+ this.startRenderLoop()
1769
+ }
1770
+ }
1771
+
1772
+ public pause(): void {
1773
+ this._controlState = RendererControlState.EXPLICIT_PAUSED
1774
+ this.internalPause()
1775
+ }
1776
+
1777
+ public suspend(): void {
1778
+ this._previousControlState = this._controlState
1779
+
1780
+ this._controlState = RendererControlState.EXPLICIT_SUSPENDED
1781
+ this.internalPause()
1782
+
1783
+ this._suspendedMouseEnabled = this._useMouse
1784
+
1785
+ this.disableMouse()
1786
+ this.removeExitListeners()
1787
+ this.stdinParser?.reset()
1788
+ this.stdin.removeListener("data", this.stdinListener)
1789
+ this.lib.suspendRenderer(this.rendererPtr)
1790
+
1791
+ if (this.stdin.setRawMode) {
1792
+ this.stdin.setRawMode(false)
1793
+ }
1794
+
1795
+ this.stdin.pause()
1796
+ }
1797
+
1798
+ public resume(): void {
1799
+ if (this.stdin.setRawMode) {
1800
+ this.stdin.setRawMode(true)
1801
+ }
1802
+
1803
+ this.stdin.resume()
1804
+ this.addExitListeners()
1805
+
1806
+ setImmediate(() => {
1807
+ // Consume any existing stdin data to avoid processing stale input
1808
+ while (this.stdin.read() !== null) {}
1809
+ this.stdin.on("data", this.stdinListener)
1810
+ })
1811
+
1812
+ this.lib.resumeRenderer(this.rendererPtr)
1813
+
1814
+ if (this._suspendedMouseEnabled) {
1815
+ this.enableMouse()
1816
+ }
1817
+
1818
+ this.currentRenderBuffer.clear(this.backgroundColor)
1819
+ this._controlState = this._previousControlState
1820
+
1821
+ if (
1822
+ this._previousControlState === RendererControlState.AUTO_STARTED ||
1823
+ this._previousControlState === RendererControlState.EXPLICIT_STARTED
1824
+ ) {
1825
+ this.internalStart()
1826
+ } else {
1827
+ this.requestRender()
1828
+ }
1829
+ }
1830
+
1831
+ private internalPause(): void {
1832
+ this._isRunning = false
1833
+
1834
+ if (this.renderTimeout) {
1835
+ this.clock.clearTimeout(this.renderTimeout)
1836
+ this.renderTimeout = null
1837
+ }
1838
+
1839
+ if (!this.rendering) {
1840
+ this.resolveIdleIfNeeded()
1841
+ }
1842
+ }
1843
+
1844
+ public stop(): void {
1845
+ this._controlState = RendererControlState.EXPLICIT_STOPPED
1846
+ this.internalStop()
1847
+ }
1848
+
1849
+ private internalStop(): void {
1850
+ if (this.isRunning && !this._isDestroyed) {
1851
+ this._isRunning = false
1852
+
1853
+ if (this.memorySnapshotTimer) {
1854
+ this.clock.clearInterval(this.memorySnapshotTimer)
1855
+ this.memorySnapshotTimer = null
1856
+ }
1857
+
1858
+ if (this.renderTimeout) {
1859
+ this.clock.clearTimeout(this.renderTimeout)
1860
+ this.renderTimeout = null
1861
+ }
1862
+
1863
+ // If we're currently rendering, the frame will resolve idle when it completes
1864
+ // Otherwise, resolve immediately
1865
+ if (!this.rendering) {
1866
+ this.resolveIdleIfNeeded()
1867
+ }
1868
+ }
1869
+ }
1870
+
1871
+ public destroy(): void {
1872
+ if (this._isDestroyed) return
1873
+ this._isDestroyed = true
1874
+ this._destroyPending = true
1875
+
1876
+ if (this.rendering) {
1877
+ // Defer teardown until the active frame completes to avoid freeing native resources mid-render.
1878
+ return
1879
+ }
1880
+
1881
+ this.finalizeDestroy()
1882
+ }
1883
+
1884
+ private finalizeDestroy(): void {
1885
+ if (this._destroyFinalized) return
1886
+ this._destroyFinalized = true
1887
+ this._destroyPending = false
1888
+
1889
+ process.removeListener("SIGWINCH", this.sigwinchHandler)
1890
+ process.removeListener("uncaughtException", this.handleError)
1891
+ process.removeListener("unhandledRejection", this.handleError)
1892
+ process.removeListener("warning", this.warningHandler)
1893
+ process.removeListener("beforeExit", this.exitHandler)
1894
+ capture.removeListener("write", this.captureCallback)
1895
+ this.removeExitListeners()
1896
+
1897
+ if (this.resizeTimeoutId !== null) {
1898
+ this.clock.clearTimeout(this.resizeTimeoutId)
1899
+ this.resizeTimeoutId = null
1900
+ }
1901
+
1902
+ if (this.capabilityTimeoutId !== null) {
1903
+ this.clock.clearTimeout(this.capabilityTimeoutId)
1904
+ this.capabilityTimeoutId = null
1905
+ }
1906
+
1907
+ if (this.memorySnapshotTimer) {
1908
+ this.clock.clearInterval(this.memorySnapshotTimer)
1909
+ }
1910
+
1911
+ // Clean up palette detector
1912
+ if (this._paletteDetector) {
1913
+ this._paletteDetector.cleanup()
1914
+ this._paletteDetector = null
1915
+ }
1916
+ this._paletteDetectionPromise = null
1917
+ this._cachedPalette = null
1918
+
1919
+ this.emit(CliRenderEvents.DESTROY)
1920
+
1921
+ if (this.renderTimeout) {
1922
+ this.clock.clearTimeout(this.renderTimeout)
1923
+ this.renderTimeout = null
1924
+ }
1925
+ this._isRunning = false
1926
+
1927
+ this.waitingForPixelResolution = false
1928
+ this.setCapturedRenderable(undefined)
1929
+
1930
+ try {
1931
+ this.root.destroyRecursively()
1932
+ } catch (e) {
1933
+ console.error("Error destroying root renderable:", e instanceof Error ? e.stack : String(e))
1934
+ }
1935
+
1936
+ // Remove listener before destroying parser
1937
+ this.stdin.removeListener("data", this.stdinListener)
1938
+ if (this.stdin.setRawMode) {
1939
+ this.stdin.setRawMode(false)
1940
+ }
1941
+
1942
+ this.stdinParser?.destroy()
1943
+ this.stdinParser = null
1944
+ this.oscSubscribers.clear()
1945
+ this._console.destroy()
1946
+ this.disableStdoutInterception()
1947
+
1948
+ if (this._splitHeight > 0) {
1949
+ this.flushStdoutCache(this._splitHeight, true)
1950
+ }
1951
+
1952
+ this.lib.destroyRenderer(this.rendererPtr)
1953
+ rendererTracker.removeRenderer(this)
1954
+
1955
+ if (this._onDestroy) {
1956
+ try {
1957
+ this._onDestroy()
1958
+ } catch (e) {
1959
+ console.error("Error in onDestroy callback:", e instanceof Error ? e.stack : String(e))
1960
+ }
1961
+ }
1962
+
1963
+ // Resolve any pending idle() calls
1964
+ this.resolveIdleIfNeeded()
1965
+ }
1966
+
1967
+ private startRenderLoop(): void {
1968
+ if (!this._isRunning) return
1969
+
1970
+ this.lastTime = this.clock.now()
1971
+ this.frameCount = 0
1972
+ this.lastFpsTime = this.lastTime
1973
+ this.currentFps = 0
1974
+
1975
+ this.loop()
1976
+ }
1977
+
1978
+ private async loop(): Promise<void> {
1979
+ if (this.rendering || this._isDestroyed) return
1980
+ this.renderTimeout = null
1981
+
1982
+ this.rendering = true
1983
+ if (this.renderTimeout) {
1984
+ this.clock.clearTimeout(this.renderTimeout)
1985
+ this.renderTimeout = null
1986
+ }
1987
+ try {
1988
+ const now = this.clock.now()
1989
+ const elapsed = now - this.lastTime
1990
+
1991
+ const deltaTime = elapsed
1992
+ this.lastTime = now
1993
+
1994
+ this.frameCount++
1995
+ if (now - this.lastFpsTime >= 1000) {
1996
+ this.currentFps = this.frameCount
1997
+ this.frameCount = 0
1998
+ this.lastFpsTime = now
1999
+ }
2000
+
2001
+ this.renderStats.frameCount++
2002
+ this.renderStats.fps = this.currentFps
2003
+ const overallStart = performance.now()
2004
+
2005
+ const frameRequests = Array.from(this.animationRequest.values())
2006
+ this.animationRequest.clear()
2007
+ const animationRequestStart = performance.now()
2008
+ for (const callback of frameRequests) {
2009
+ callback(deltaTime)
2010
+ this.dropLive()
2011
+ }
2012
+ const animationRequestEnd = performance.now()
2013
+ const animationRequestTime = animationRequestEnd - animationRequestStart
2014
+
2015
+ const start = performance.now()
2016
+ for (const frameCallback of this.frameCallbacks) {
2017
+ try {
2018
+ await frameCallback(deltaTime)
2019
+ } catch (error) {
2020
+ console.error("Error in frame callback:", error)
2021
+ }
2022
+ }
2023
+ const end = performance.now()
2024
+ this.renderStats.frameCallbackTime = end - start
2025
+
2026
+ this.root.render(this.nextRenderBuffer, deltaTime)
2027
+
2028
+ for (const postProcessFn of this.postProcessFns) {
2029
+ postProcessFn(this.nextRenderBuffer, deltaTime)
2030
+ }
2031
+
2032
+ this._console.renderToBuffer(this.nextRenderBuffer)
2033
+
2034
+ // If destroy() was requested during this frame, skip native work and scheduling.
2035
+ if (!this._isDestroyed) {
2036
+ this.renderNative()
2037
+
2038
+ // Check if hit grid changed and recheck hover state if needed
2039
+ if (this._useMouse && this.lib.getHitGridDirty(this.rendererPtr)) {
2040
+ this.recheckHoverState()
2041
+ }
2042
+
2043
+ const overallFrameTime = performance.now() - overallStart
2044
+
2045
+ // TODO: Add animationRequestTime to stats
2046
+ this.lib.updateStats(
2047
+ this.rendererPtr,
2048
+ overallFrameTime,
2049
+ this.renderStats.fps,
2050
+ this.renderStats.frameCallbackTime,
2051
+ )
2052
+
2053
+ if (this.gatherStats) {
2054
+ this.collectStatSample(overallFrameTime)
2055
+ }
2056
+
2057
+ if (this._isRunning || this.immediateRerenderRequested) {
2058
+ const targetFrameTime = this.immediateRerenderRequested ? this.minTargetFrameTime : this.targetFrameTime
2059
+ const delay = Math.max(1, targetFrameTime - Math.floor(overallFrameTime))
2060
+ this.immediateRerenderRequested = false
2061
+ this.renderTimeout = this.clock.setTimeout(() => {
2062
+ this.renderTimeout = null
2063
+ this.loop()
2064
+ }, delay)
2065
+ } else {
2066
+ this.clock.clearTimeout(this.renderTimeout!)
2067
+ this.renderTimeout = null
2068
+ }
2069
+ }
2070
+ } finally {
2071
+ this.rendering = false
2072
+ if (this._destroyPending) {
2073
+ this.finalizeDestroy()
2074
+ }
2075
+ this.resolveIdleIfNeeded()
2076
+ }
2077
+ }
2078
+
2079
+ public intermediateRender(): void {
2080
+ this.immediateRerenderRequested = true
2081
+ this.loop()
2082
+ }
2083
+
2084
+ private renderNative(): void {
2085
+ if (this.renderingNative) {
2086
+ console.error("Rendering called concurrently")
2087
+ throw new Error("Rendering called concurrently")
2088
+ }
2089
+
2090
+ let force = false
2091
+ if (this._splitHeight > 0) {
2092
+ // TODO: Flickering could maybe be even more reduced by moving the flush to the native layer,
2093
+ // to output the flush with the buffered writer, after the render is done.
2094
+ force = this.flushStdoutCache(this._splitHeight)
2095
+ }
2096
+
2097
+ this.renderingNative = true
2098
+ this.lib.render(this.rendererPtr, force)
2099
+ // this.dumpStdoutBuffer(Date.now())
2100
+ this.renderingNative = false
2101
+ }
2102
+
2103
+ private collectStatSample(frameTime: number): void {
2104
+ this.frameTimes.push(frameTime)
2105
+ if (this.frameTimes.length > this.maxStatSamples) {
2106
+ this.frameTimes.shift()
2107
+ }
2108
+ }
2109
+
2110
+ public getStats(): {
2111
+ fps: number
2112
+ frameCount: number
2113
+ frameTimes: number[]
2114
+ averageFrameTime: number
2115
+ minFrameTime: number
2116
+ maxFrameTime: number
2117
+ } {
2118
+ const frameTimes = [...this.frameTimes]
2119
+ const sum = frameTimes.reduce((acc, time) => acc + time, 0)
2120
+ const avg = frameTimes.length ? sum / frameTimes.length : 0
2121
+ const min = frameTimes.length ? Math.min(...frameTimes) : 0
2122
+ const max = frameTimes.length ? Math.max(...frameTimes) : 0
2123
+
2124
+ return {
2125
+ fps: this.renderStats.fps,
2126
+ frameCount: this.renderStats.frameCount,
2127
+ frameTimes,
2128
+ averageFrameTime: avg,
2129
+ minFrameTime: min,
2130
+ maxFrameTime: max,
2131
+ }
2132
+ }
2133
+
2134
+ public resetStats(): void {
2135
+ this.frameTimes = []
2136
+ this.renderStats.frameCount = 0
2137
+ }
2138
+
2139
+ public setGatherStats(enabled: boolean): void {
2140
+ this.gatherStats = enabled
2141
+ if (!enabled) {
2142
+ this.frameTimes = []
2143
+ }
2144
+ }
2145
+
2146
+ public getSelection(): Selection | null {
2147
+ return this.currentSelection
2148
+ }
2149
+
2150
+ public get hasSelection(): boolean {
2151
+ return !!this.currentSelection
2152
+ }
2153
+
2154
+ public getSelectionContainer(): Renderable | null {
2155
+ return this.selectionContainers.length > 0 ? this.selectionContainers[this.selectionContainers.length - 1] : null
2156
+ }
2157
+
2158
+ public clearSelection(): void {
2159
+ if (this.currentSelection) {
2160
+ for (const renderable of this.currentSelection.touchedRenderables) {
2161
+ if (renderable.selectable && !renderable.isDestroyed) {
2162
+ renderable.onSelectionChanged(null)
2163
+ }
2164
+ }
2165
+ this.currentSelection = null
2166
+ }
2167
+ this.selectionContainers = []
2168
+ }
2169
+
2170
+ /**
2171
+ * Start a new selection at the given coordinates.
2172
+ * Used by both mouse and keyboard selection.
2173
+ */
2174
+ public startSelection(renderable: Renderable, x: number, y: number): void {
2175
+ if (!renderable.selectable) return
2176
+
2177
+ this.clearSelection()
2178
+ this.selectionContainers.push(renderable.parent || this.root)
2179
+ this.currentSelection = new Selection(renderable, { x, y }, { x, y })
2180
+ this.currentSelection.isStart = true
2181
+
2182
+ this.notifySelectablesOfSelectionChange()
2183
+ }
2184
+
2185
+ public updateSelection(
2186
+ currentRenderable: Renderable | undefined,
2187
+ x: number,
2188
+ y: number,
2189
+ options?: { finishDragging?: boolean },
2190
+ ): void {
2191
+ if (this.currentSelection) {
2192
+ this.currentSelection.isStart = false
2193
+ this.currentSelection.focus = { x, y }
2194
+
2195
+ if (options?.finishDragging) {
2196
+ this.currentSelection.isDragging = false
2197
+ }
2198
+
2199
+ if (this.selectionContainers.length > 0) {
2200
+ const currentContainer = this.selectionContainers[this.selectionContainers.length - 1]
2201
+
2202
+ if (!currentRenderable || !this.isWithinContainer(currentRenderable, currentContainer)) {
2203
+ const parentContainer = currentContainer.parent || this.root
2204
+ this.selectionContainers.push(parentContainer)
2205
+ } else if (currentRenderable && this.selectionContainers.length > 1) {
2206
+ let containerIndex = this.selectionContainers.indexOf(currentRenderable)
2207
+
2208
+ if (containerIndex === -1) {
2209
+ const immediateParent = currentRenderable.parent || this.root
2210
+ containerIndex = this.selectionContainers.indexOf(immediateParent)
2211
+ }
2212
+
2213
+ if (containerIndex !== -1 && containerIndex < this.selectionContainers.length - 1) {
2214
+ this.selectionContainers = this.selectionContainers.slice(0, containerIndex + 1)
2215
+ }
2216
+ }
2217
+ }
2218
+
2219
+ this.notifySelectablesOfSelectionChange()
2220
+ }
2221
+ }
2222
+
2223
+ public requestSelectionUpdate(): void {
2224
+ if (this.currentSelection?.isDragging) {
2225
+ const pointer = this._latestPointer
2226
+
2227
+ const maybeRenderableId = this.hitTest(pointer.x, pointer.y)
2228
+ const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)
2229
+
2230
+ this.updateSelection(maybeRenderable, pointer.x, pointer.y)
2231
+ }
2232
+ }
2233
+
2234
+ private isWithinContainer(renderable: Renderable, container: Renderable): boolean {
2235
+ let current: Renderable | null = renderable
2236
+ while (current) {
2237
+ if (current === container) return true
2238
+ current = current.parent
2239
+ }
2240
+ return false
2241
+ }
2242
+
2243
+ private finishSelection(): void {
2244
+ if (this.currentSelection) {
2245
+ this.currentSelection.isDragging = false
2246
+ this.emit("selection", this.currentSelection)
2247
+ this.notifySelectablesOfSelectionChange()
2248
+ }
2249
+ }
2250
+
2251
+ private notifySelectablesOfSelectionChange(): void {
2252
+ const selectedRenderables: Renderable[] = []
2253
+ const touchedRenderables: Renderable[] = []
2254
+ const currentContainer =
2255
+ this.selectionContainers.length > 0 ? this.selectionContainers[this.selectionContainers.length - 1] : this.root
2256
+
2257
+ if (this.currentSelection) {
2258
+ this.walkSelectableRenderables(
2259
+ currentContainer,
2260
+ this.currentSelection.bounds,
2261
+ selectedRenderables,
2262
+ touchedRenderables,
2263
+ )
2264
+
2265
+ for (const renderable of this.currentSelection.touchedRenderables) {
2266
+ if (!touchedRenderables.includes(renderable) && !renderable.isDestroyed) {
2267
+ renderable.onSelectionChanged(null)
2268
+ }
2269
+ }
2270
+
2271
+ this.currentSelection.updateSelectedRenderables(selectedRenderables)
2272
+ this.currentSelection.updateTouchedRenderables(touchedRenderables)
2273
+ }
2274
+ }
2275
+
2276
+ private walkSelectableRenderables(
2277
+ container: Renderable,
2278
+ selectionBounds: ViewportBounds,
2279
+ selectedRenderables: Renderable[],
2280
+ touchedRenderables: Renderable[],
2281
+ ): void {
2282
+ const children = getObjectsInViewport<Renderable>(
2283
+ selectionBounds,
2284
+ container.getChildrenSortedByPrimaryAxis(),
2285
+ container.primaryAxis,
2286
+ 0, // padding
2287
+ 0, // minTriggerSize - always perform overlap checks for selection
2288
+ )
2289
+
2290
+ for (const child of children) {
2291
+ if (child.selectable) {
2292
+ const hasSelection = child.onSelectionChanged(this.currentSelection)
2293
+ if (hasSelection) {
2294
+ selectedRenderables.push(child)
2295
+ }
2296
+ touchedRenderables.push(child)
2297
+ }
2298
+ if (child.getChildrenCount() > 0) {
2299
+ this.walkSelectableRenderables(child, selectionBounds, selectedRenderables, touchedRenderables)
2300
+ }
2301
+ }
2302
+ }
2303
+
2304
+ public get paletteDetectionStatus(): "idle" | "detecting" | "cached" {
2305
+ if (this._cachedPalette) return "cached"
2306
+ if (this._paletteDetectionPromise) return "detecting"
2307
+ return "idle"
2308
+ }
2309
+
2310
+ public clearPaletteCache(): void {
2311
+ this._cachedPalette = null
2312
+ }
2313
+
2314
+ /**
2315
+ * Detects the terminal's color palette
2316
+ *
2317
+ * @returns Promise resolving to TerminalColors object containing palette and special colors
2318
+ * @throws Error if renderer is suspended
2319
+ */
2320
+ public async getPalette(options?: GetPaletteOptions): Promise<TerminalColors> {
2321
+ if (this._controlState === RendererControlState.EXPLICIT_SUSPENDED) {
2322
+ throw new Error("Cannot detect palette while renderer is suspended")
2323
+ }
2324
+
2325
+ const requestedSize = options?.size ?? 16
2326
+
2327
+ if (this._cachedPalette && this._cachedPalette.palette.length !== requestedSize) {
2328
+ this._cachedPalette = null
2329
+ }
2330
+
2331
+ if (this._cachedPalette) {
2332
+ return this._cachedPalette
2333
+ }
2334
+
2335
+ if (this._paletteDetectionPromise) {
2336
+ return this._paletteDetectionPromise
2337
+ }
2338
+
2339
+ if (!this._paletteDetector) {
2340
+ const isLegacyTmux =
2341
+ this.capabilities?.terminal?.name?.toLowerCase()?.includes("tmux") &&
2342
+ this.capabilities?.terminal?.version?.localeCompare("3.6") < 0
2343
+ this._paletteDetector = createTerminalPalette(
2344
+ this.stdin,
2345
+ this.stdout,
2346
+ this.writeOut.bind(this),
2347
+ isLegacyTmux,
2348
+ {
2349
+ subscribeOsc: this.subscribeOsc.bind(this),
2350
+ },
2351
+ this.clock,
2352
+ )
2353
+ }
2354
+
2355
+ this._paletteDetectionPromise = this._paletteDetector.detect(options).then((result) => {
2356
+ this._cachedPalette = result
2357
+ this._paletteDetectionPromise = null
2358
+ return result
2359
+ })
2360
+
2361
+ return this._paletteDetectionPromise
2362
+ }
2363
+ }