@fairyhunter13/opentui-core 0.1.113 → 0.1.114

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