@xincli/opentui-core 0.4.6

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 (476) hide show
  1. package/README.md +63 -0
  2. package/dev/keypress-debug-renderer.ts +148 -0
  3. package/dev/keypress-debug.ts +43 -0
  4. package/dev/print-env-vars.ts +32 -0
  5. package/dev/test-tmux-graphics-334.sh +68 -0
  6. package/dev/test-tmux-theme-queries.sh +151 -0
  7. package/dev/thai-debug-test.ts +68 -0
  8. package/dev/theme-mode.ts +290 -0
  9. package/docs/development.md +144 -0
  10. package/package.json +102 -0
  11. package/prebuilt/aarch64-android/libopentui.so +0 -0
  12. package/scripts/build-android.ts +226 -0
  13. package/scripts/build-native-termux.sh +525 -0
  14. package/scripts/build.ts +530 -0
  15. package/scripts/dist-test.ts +312 -0
  16. package/scripts/package-prebuilt.ts +110 -0
  17. package/scripts/publish.ts +60 -0
  18. package/scripts/test-node-hook.mjs +16 -0
  19. package/scripts/test-node.ts +241 -0
  20. package/scripts/vendor-deps.sh +73 -0
  21. package/src/NativeSpanFeed.ts +375 -0
  22. package/src/Renderable.ts +1878 -0
  23. package/src/__snapshots__/buffer.test.ts.snap +28 -0
  24. package/src/animation/Timeline.test.ts +2709 -0
  25. package/src/animation/Timeline.ts +598 -0
  26. package/src/ansi.ts +22 -0
  27. package/src/audio.ts +479 -0
  28. package/src/benchmark/attenuation-benchmark.ts +81 -0
  29. package/src/benchmark/audio-playback-benchmark.ts +344 -0
  30. package/src/benchmark/box-draw-benchmark.ts +1042 -0
  31. package/src/benchmark/colormatrix-benchmark.ts +123 -0
  32. package/src/benchmark/gain-benchmark.ts +80 -0
  33. package/src/benchmark/latest-all-bench-run.json +707 -0
  34. package/src/benchmark/latest-async-bench-run.json +336 -0
  35. package/src/benchmark/latest-default-bench-run.json +657 -0
  36. package/src/benchmark/latest-large-bench-run.json +707 -0
  37. package/src/benchmark/latest-quick-bench-run.json +207 -0
  38. package/src/benchmark/layout-benchmark.ts +2547 -0
  39. package/src/benchmark/markdown-benchmark.ts +1796 -0
  40. package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
  41. package/src/benchmark/native-span-feed-benchmark.md +56 -0
  42. package/src/benchmark/native-span-feed-benchmark.ts +596 -0
  43. package/src/benchmark/native-span-feed-compare.ts +280 -0
  44. package/src/benchmark/render-traversal-benchmark.ts +928 -0
  45. package/src/benchmark/text-buffer-render-benchmark.ts +874 -0
  46. package/src/benchmark/text-table-benchmark.ts +948 -0
  47. package/src/buffer.test.ts +292 -0
  48. package/src/buffer.ts +564 -0
  49. package/src/console.test.ts +629 -0
  50. package/src/console.ts +1253 -0
  51. package/src/edit-buffer.test.ts +1852 -0
  52. package/src/edit-buffer.ts +417 -0
  53. package/src/editor-view.test.ts +1056 -0
  54. package/src/editor-view.ts +290 -0
  55. package/src/index.ts +25 -0
  56. package/src/lib/KeyHandler.integration.test.ts +292 -0
  57. package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
  58. package/src/lib/KeyHandler.test.ts +662 -0
  59. package/src/lib/KeyHandler.ts +222 -0
  60. package/src/lib/RGBA.test.ts +60 -0
  61. package/src/lib/RGBA.ts +343 -0
  62. package/src/lib/ascii.font.ts +330 -0
  63. package/src/lib/border.test.ts +83 -0
  64. package/src/lib/border.ts +170 -0
  65. package/src/lib/bunfs.test.ts +27 -0
  66. package/src/lib/bunfs.ts +18 -0
  67. package/src/lib/clipboard.test.ts +63 -0
  68. package/src/lib/clipboard.ts +41 -0
  69. package/src/lib/clock.ts +35 -0
  70. package/src/lib/data-paths.test.ts +133 -0
  71. package/src/lib/data-paths.ts +109 -0
  72. package/src/lib/debounce.ts +106 -0
  73. package/src/lib/detect-links.test.ts +98 -0
  74. package/src/lib/detect-links.ts +56 -0
  75. package/src/lib/env.test.ts +228 -0
  76. package/src/lib/env.ts +209 -0
  77. package/src/lib/extmarks-history.ts +51 -0
  78. package/src/lib/extmarks-multiwidth.test.ts +324 -0
  79. package/src/lib/extmarks.test.ts +3547 -0
  80. package/src/lib/extmarks.ts +861 -0
  81. package/src/lib/fonts/block.json +405 -0
  82. package/src/lib/fonts/grid.json +265 -0
  83. package/src/lib/fonts/huge.json +741 -0
  84. package/src/lib/fonts/pallet.json +314 -0
  85. package/src/lib/fonts/shade.json +591 -0
  86. package/src/lib/fonts/slick.json +321 -0
  87. package/src/lib/fonts/tiny.json +69 -0
  88. package/src/lib/hast-styled-text.ts +59 -0
  89. package/src/lib/index.ts +22 -0
  90. package/src/lib/keybinding.internal.test.ts +342 -0
  91. package/src/lib/keybinding.internal.ts +182 -0
  92. package/src/lib/objects-in-viewport.test.ts +789 -0
  93. package/src/lib/objects-in-viewport.ts +153 -0
  94. package/src/lib/output.capture.ts +58 -0
  95. package/src/lib/parse.keypress-kitty.protocol.test.ts +347 -0
  96. package/src/lib/parse.keypress-kitty.test.ts +712 -0
  97. package/src/lib/parse.keypress-kitty.ts +470 -0
  98. package/src/lib/parse.keypress.test.ts +2020 -0
  99. package/src/lib/parse.keypress.ts +480 -0
  100. package/src/lib/parse.mouse.test.ts +552 -0
  101. package/src/lib/parse.mouse.ts +232 -0
  102. package/src/lib/paste.ts +18 -0
  103. package/src/lib/queue.ts +65 -0
  104. package/src/lib/render-geometry.ts +36 -0
  105. package/src/lib/renderable.validations.test.ts +87 -0
  106. package/src/lib/renderable.validations.ts +83 -0
  107. package/src/lib/scroll-acceleration.ts +98 -0
  108. package/src/lib/selection.ts +261 -0
  109. package/src/lib/singleton.ts +34 -0
  110. package/src/lib/stdin-parser.test.ts +2439 -0
  111. package/src/lib/stdin-parser.ts +2007 -0
  112. package/src/lib/styled-text.ts +178 -0
  113. package/src/lib/terminal-capability-detection.test.ts +226 -0
  114. package/src/lib/terminal-capability-detection.ts +98 -0
  115. package/src/lib/terminal-palette.test.ts +954 -0
  116. package/src/lib/terminal-palette.ts +441 -0
  117. package/src/lib/tree-sitter/assets/README.md +119 -0
  118. package/src/lib/tree-sitter/assets/javascript/highlights.scm +205 -0
  119. package/src/lib/tree-sitter/assets/javascript/tree-sitter-javascript.wasm +0 -0
  120. package/src/lib/tree-sitter/assets/markdown/highlights.scm +150 -0
  121. package/src/lib/tree-sitter/assets/markdown/injections.scm +27 -0
  122. package/src/lib/tree-sitter/assets/markdown/tree-sitter-markdown.wasm +0 -0
  123. package/src/lib/tree-sitter/assets/markdown_inline/highlights.scm +115 -0
  124. package/src/lib/tree-sitter/assets/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  125. package/src/lib/tree-sitter/assets/typescript/highlights.scm +604 -0
  126. package/src/lib/tree-sitter/assets/typescript/tree-sitter-typescript.wasm +0 -0
  127. package/src/lib/tree-sitter/assets/update.ts +360 -0
  128. package/src/lib/tree-sitter/assets/zig/highlights.scm +284 -0
  129. package/src/lib/tree-sitter/assets/zig/tree-sitter-zig.wasm +0 -0
  130. package/src/lib/tree-sitter/assets.d.ts +9 -0
  131. package/src/lib/tree-sitter/cache.test.ts +291 -0
  132. package/src/lib/tree-sitter/client.test.ts +1434 -0
  133. package/src/lib/tree-sitter/client.ts +847 -0
  134. package/src/lib/tree-sitter/default-parsers.ts +133 -0
  135. package/src/lib/tree-sitter/download-utils.ts +148 -0
  136. package/src/lib/tree-sitter/index.ts +75 -0
  137. package/src/lib/tree-sitter/parser.worker.ts +1098 -0
  138. package/src/lib/tree-sitter/parsers-config.ts +81 -0
  139. package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
  140. package/src/lib/tree-sitter/resolve-ft.ts +189 -0
  141. package/src/lib/tree-sitter/types.ts +132 -0
  142. package/src/lib/tree-sitter/update-assets.ts +10 -0
  143. package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
  144. package/src/lib/tree-sitter-styled-text.ts +314 -0
  145. package/src/lib/validate-dir-name.ts +55 -0
  146. package/src/lib/yoga.options.test.ts +628 -0
  147. package/src/lib/yoga.options.ts +346 -0
  148. package/src/native-event-worker-repro.test.ts +48 -0
  149. package/src/native-event-worker-repro.worker.ts +9 -0
  150. package/src/native-handle.test.ts +221 -0
  151. package/src/platform/ffi.test.ts +516 -0
  152. package/src/platform/ffi.ts +653 -0
  153. package/src/platform/runtime.test.ts +52 -0
  154. package/src/platform/runtime.ts +121 -0
  155. package/src/platform/test.ts +45 -0
  156. package/src/platform/worker-handler-cleanup.fixture.ts +12 -0
  157. package/src/platform/worker-onmessage-startup.fixture.ts +10 -0
  158. package/src/platform/worker-startup.fixture.ts +9 -0
  159. package/src/platform/worker.node-test.ts +153 -0
  160. package/src/platform/worker.ts +425 -0
  161. package/src/plugins/core-slot.ts +579 -0
  162. package/src/plugins/registry.ts +402 -0
  163. package/src/plugins/types.ts +46 -0
  164. package/src/post/effects.ts +948 -0
  165. package/src/post/filters.ts +512 -0
  166. package/src/post/matrices.ts +288 -0
  167. package/src/renderables/ASCIIFont.ts +219 -0
  168. package/src/renderables/Box.test.ts +474 -0
  169. package/src/renderables/Box.ts +353 -0
  170. package/src/renderables/Code.test.ts +2388 -0
  171. package/src/renderables/Code.ts +555 -0
  172. package/src/renderables/Diff.regression.test.ts +381 -0
  173. package/src/renderables/Diff.test.ts +3212 -0
  174. package/src/renderables/Diff.ts +1278 -0
  175. package/src/renderables/EditBufferRenderable.test.ts +288 -0
  176. package/src/renderables/EditBufferRenderable.ts +1152 -0
  177. package/src/renderables/FrameBuffer.ts +47 -0
  178. package/src/renderables/Input.test.ts +1310 -0
  179. package/src/renderables/Input.ts +273 -0
  180. package/src/renderables/LineNumberRenderable.ts +725 -0
  181. package/src/renderables/Markdown.ts +2144 -0
  182. package/src/renderables/ScrollBar.ts +423 -0
  183. package/src/renderables/ScrollBox.ts +903 -0
  184. package/src/renderables/Select.test.ts +1134 -0
  185. package/src/renderables/Select.ts +535 -0
  186. package/src/renderables/Slider.test.ts +456 -0
  187. package/src/renderables/Slider.ts +342 -0
  188. package/src/renderables/TabSelect.test.ts +238 -0
  189. package/src/renderables/TabSelect.ts +447 -0
  190. package/src/renderables/Text.selection-buffer.test.ts +118 -0
  191. package/src/renderables/Text.test.ts +2662 -0
  192. package/src/renderables/Text.ts +147 -0
  193. package/src/renderables/TextBufferRenderable.ts +504 -0
  194. package/src/renderables/TextNode.test.ts +1137 -0
  195. package/src/renderables/TextNode.ts +379 -0
  196. package/src/renderables/TextTable.test.ts +1411 -0
  197. package/src/renderables/TextTable.ts +1434 -0
  198. package/src/renderables/Textarea.ts +425 -0
  199. package/src/renderables/TimeToFirstDraw.ts +89 -0
  200. package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
  201. package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
  202. package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
  203. package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
  204. package/src/renderables/__tests__/Code.test.ts +55 -0
  205. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
  206. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +827 -0
  207. package/src/renderables/__tests__/LineNumberRenderable.test.ts +1906 -0
  208. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
  209. package/src/renderables/__tests__/Markdown.code-colors.test.ts +288 -0
  210. package/src/renderables/__tests__/Markdown.test.ts +4175 -0
  211. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
  212. package/src/renderables/__tests__/Textarea.buffer.test.ts +683 -0
  213. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
  214. package/src/renderables/__tests__/Textarea.editing.test.ts +2120 -0
  215. package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
  216. package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
  217. package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
  218. package/src/renderables/__tests__/Textarea.keybinding.test.ts +3275 -0
  219. package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
  220. package/src/renderables/__tests__/Textarea.rendering.test.ts +1927 -0
  221. package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
  222. package/src/renderables/__tests__/Textarea.selection.test.ts +1589 -0
  223. package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
  224. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
  225. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
  226. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
  227. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
  228. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
  229. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
  230. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
  231. package/src/renderables/__tests__/markdown-parser.test.ts +241 -0
  232. package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
  233. package/src/renderables/composition/README.md +8 -0
  234. package/src/renderables/composition/VRenderable.ts +32 -0
  235. package/src/renderables/composition/constructs.ts +127 -0
  236. package/src/renderables/composition/vnode.ts +277 -0
  237. package/src/renderables/index.ts +23 -0
  238. package/src/renderables/markdown-parser.ts +79 -0
  239. package/src/renderer-theme-mode.ts +199 -0
  240. package/src/renderer.ts +4975 -0
  241. package/src/runtime-plugin-support-configure.ts +63 -0
  242. package/src/runtime-plugin-support.ts +13 -0
  243. package/src/runtime-plugin.ts +626 -0
  244. package/src/specs/terminal-startup.md +64 -0
  245. package/src/syntax-style.test.ts +841 -0
  246. package/src/syntax-style.ts +274 -0
  247. package/src/testing/README.md +219 -0
  248. package/src/testing/bun-test-node.ts +880 -0
  249. package/src/testing/capture-spans.test.ts +216 -0
  250. package/src/testing/integration.test.ts +276 -0
  251. package/src/testing/manual-clock.ts +117 -0
  252. package/src/testing/mock-keys.test.ts +1378 -0
  253. package/src/testing/mock-keys.ts +457 -0
  254. package/src/testing/mock-mouse.test.ts +218 -0
  255. package/src/testing/mock-mouse.ts +247 -0
  256. package/src/testing/mock-tree-sitter-client.ts +81 -0
  257. package/src/testing/spy.ts +13 -0
  258. package/src/testing/terminal-capabilities.ts +47 -0
  259. package/src/testing/test-recorder.test.ts +410 -0
  260. package/src/testing/test-recorder.ts +133 -0
  261. package/src/testing/test-renderer.ts +375 -0
  262. package/src/testing/test-renderer.wait.test.ts +278 -0
  263. package/src/testing/test-streams.ts +31 -0
  264. package/src/testing.ts +9 -0
  265. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
  266. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
  267. package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
  268. package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
  269. package/src/tests/allocator-stats.test.ts +53 -0
  270. package/src/tests/audio.test.ts +303 -0
  271. package/src/tests/destroy-during-render.test.ts +200 -0
  272. package/src/tests/destroy-on-exit.fixture.ts +36 -0
  273. package/src/tests/destroy-on-exit.test.ts +62 -0
  274. package/src/tests/ffi-borrowed-pointer-callsites.test.ts +153 -0
  275. package/src/tests/hover-cursor.test.ts +98 -0
  276. package/src/tests/native-backed-measurement-lifecycle.test.ts +133 -0
  277. package/src/tests/native-backed-measurement-parity.test.ts +322 -0
  278. package/src/tests/native-span-feed-async.test.ts +173 -0
  279. package/src/tests/native-span-feed-close.test.ts +120 -0
  280. package/src/tests/native-span-feed-coverage.test.ts +227 -0
  281. package/src/tests/native-span-feed-edge-cases.test.ts +384 -0
  282. package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
  283. package/src/tests/opacity.test.ts +123 -0
  284. package/src/tests/renderable.snapshot.test.ts +524 -0
  285. package/src/tests/renderable.test.ts +1851 -0
  286. package/src/tests/renderer.clock.test.ts +270 -0
  287. package/src/tests/renderer.console-startup.test.ts +2151 -0
  288. package/src/tests/renderer.control.test.ts +458 -0
  289. package/src/tests/renderer.core-slot-binding.test.ts +962 -0
  290. package/src/tests/renderer.cursor.test.ts +26 -0
  291. package/src/tests/renderer.custom-stdout.test.ts +1112 -0
  292. package/src/tests/renderer.destroy-during-render.test.ts +147 -0
  293. package/src/tests/renderer.focus-restore.test.ts +257 -0
  294. package/src/tests/renderer.focus.test.ts +329 -0
  295. package/src/tests/renderer.idle.test.ts +219 -0
  296. package/src/tests/renderer.input.test.ts +2615 -0
  297. package/src/tests/renderer.kitty-flags.test.ts +195 -0
  298. package/src/tests/renderer.lifecycle.test.ts +34 -0
  299. package/src/tests/renderer.mouse.test.ts +1315 -0
  300. package/src/tests/renderer.notifications.test.ts +27 -0
  301. package/src/tests/renderer.palette.test.ts +1199 -0
  302. package/src/tests/renderer.remote.test.ts +247 -0
  303. package/src/tests/renderer.render-stats.test.ts +146 -0
  304. package/src/tests/renderer.scrollback-surface.test.ts +474 -0
  305. package/src/tests/renderer.selection.test.ts +138 -0
  306. package/src/tests/renderer.slot-registry.test.ts +684 -0
  307. package/src/tests/renderer.tracker.test.ts +202 -0
  308. package/src/tests/renderer.useMouse.test.ts +83 -0
  309. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
  310. package/src/tests/runtime-plugin-node-modules-import-like-string.fixture.ts +48 -0
  311. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
  312. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
  313. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
  314. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
  315. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
  316. package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
  317. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
  318. package/src/tests/runtime-plugin-support-configure.fixture.ts +31 -0
  319. package/src/tests/runtime-plugin-support-late-addition.fixture.ts +17 -0
  320. package/src/tests/runtime-plugin-support.fixture.ts +11 -0
  321. package/src/tests/runtime-plugin-support.test.ts +54 -0
  322. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
  323. package/src/tests/runtime-plugin.fixture.ts +40 -0
  324. package/src/tests/runtime-plugin.test.ts +369 -0
  325. package/src/tests/scrollbox-culling-bug.test.ts +114 -0
  326. package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
  327. package/src/tests/scrollbox-hitgrid.test.ts +909 -0
  328. package/src/tests/scrollbox.test.ts +1895 -0
  329. package/src/tests/wrap-resize-perf.test.ts +276 -0
  330. package/src/tests/yoga-api-coverage.test.ts +409 -0
  331. package/src/tests/yoga-callback-stress.test.ts +122 -0
  332. package/src/tests/yoga-ffi.test.ts +117 -0
  333. package/src/tests/yoga-setters.test.ts +921 -0
  334. package/src/tests/yoga-upstream/YGAlignBaselineTest.test.ts +124 -0
  335. package/src/tests/yoga-upstream/YGComputedBorderTest.test.ts +27 -0
  336. package/src/tests/yoga-upstream/YGComputedMarginTest.test.ts +27 -0
  337. package/src/tests/yoga-upstream/YGComputedPaddingTest.test.ts +27 -0
  338. package/src/tests/yoga-upstream/YGDirtiedTest.test.ts +157 -0
  339. package/src/tests/yoga-upstream/YGErrataTest.test.ts +40 -0
  340. package/src/tests/yoga-upstream/YGFlexBasisAuto.test.ts +23 -0
  341. package/src/tests/yoga-upstream/YGHasNewLayout.test.ts +81 -0
  342. package/src/tests/yoga-upstream/YGMeasureCacheTest.test.ts +31 -0
  343. package/src/tests/yoga-upstream/YGMeasureTest.test.ts +70 -0
  344. package/src/tests/yoga-upstream/generated/YGAbsolutePositionTest.test.ts +1864 -0
  345. package/src/tests/yoga-upstream/generated/YGAlignContentTest.test.ts +5829 -0
  346. package/src/tests/yoga-upstream/generated/YGAlignItemsTest.test.ts +2444 -0
  347. package/src/tests/yoga-upstream/generated/YGAlignSelfTest.test.ts +293 -0
  348. package/src/tests/yoga-upstream/generated/YGAndroidNewsFeed.test.ts +315 -0
  349. package/src/tests/yoga-upstream/generated/YGAspectRatioTest.test.ts +231 -0
  350. package/src/tests/yoga-upstream/generated/YGAutoTest.test.ts +347 -0
  351. package/src/tests/yoga-upstream/generated/YGBorderTest.test.ts +256 -0
  352. package/src/tests/yoga-upstream/generated/YGBoxSizingTest.test.ts +2747 -0
  353. package/src/tests/yoga-upstream/generated/YGDimensionTest.test.ts +129 -0
  354. package/src/tests/yoga-upstream/generated/YGDisplayContentsTest.test.ts +109 -0
  355. package/src/tests/yoga-upstream/generated/YGDisplayTest.test.ts +1097 -0
  356. package/src/tests/yoga-upstream/generated/YGFlexDirectionTest.test.ts +4562 -0
  357. package/src/tests/yoga-upstream/generated/YGFlexTest.test.ts +677 -0
  358. package/src/tests/yoga-upstream/generated/YGFlexWrapTest.test.ts +2071 -0
  359. package/src/tests/yoga-upstream/generated/YGGapTest.test.ts +3303 -0
  360. package/src/tests/yoga-upstream/generated/YGIntrinsicSizeTest.test.ts +582 -0
  361. package/src/tests/yoga-upstream/generated/YGJustifyContentTest.test.ts +2150 -0
  362. package/src/tests/yoga-upstream/generated/YGMarginTest.test.ts +1963 -0
  363. package/src/tests/yoga-upstream/generated/YGMinMaxDimensionTest.test.ts +1446 -0
  364. package/src/tests/yoga-upstream/generated/YGPaddingTest.test.ts +354 -0
  365. package/src/tests/yoga-upstream/generated/YGPercentageTest.test.ts +1760 -0
  366. package/src/tests/yoga-upstream/generated/YGRoundingTest.test.ts +1161 -0
  367. package/src/tests/yoga-upstream/generated/YGSizeOverflowTest.test.ts +207 -0
  368. package/src/tests/yoga-upstream/generated/YGStaticPositionTest.test.ts +6338 -0
  369. package/src/tests/yoga-upstream/tools/MeasureCounter.ts +54 -0
  370. package/src/tests/yoga-upstream/tools/utils.ts +81 -0
  371. package/src/text-buffer-view.test.ts +722 -0
  372. package/src/text-buffer-view.ts +193 -0
  373. package/src/text-buffer.test.ts +512 -0
  374. package/src/text-buffer.ts +250 -0
  375. package/src/types.ts +199 -0
  376. package/src/utils.ts +88 -0
  377. package/src/yoga.ts +1146 -0
  378. package/src/zig/ansi.zig +502 -0
  379. package/src/zig/audio.zig +1206 -0
  380. package/src/zig/bench/README.md +50 -0
  381. package/src/zig/bench/buffer-color-blending_bench.zig +350 -0
  382. package/src/zig/bench/buffer-draw-box_bench.zig +786 -0
  383. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +894 -0
  384. package/src/zig/bench/edit-buffer_bench.zig +476 -0
  385. package/src/zig/bench/editor-view_bench.zig +169 -0
  386. package/src/zig/bench/native-span-feed_bench.zig +107 -0
  387. package/src/zig/bench/rope-markers_bench.zig +713 -0
  388. package/src/zig/bench/rope_bench.zig +514 -0
  389. package/src/zig/bench/styled-text_bench.zig +476 -0
  390. package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
  391. package/src/zig/bench/text-buffer-view_bench.zig +459 -0
  392. package/src/zig/bench/text-chunk-graphemes_bench.zig +272 -0
  393. package/src/zig/bench/utf8_bench.zig +799 -0
  394. package/src/zig/bench-utils.zig +431 -0
  395. package/src/zig/bench.zig +223 -0
  396. package/src/zig/buffer-methods.zig +215 -0
  397. package/src/zig/buffer.zig +2515 -0
  398. package/src/zig/build.zig +700 -0
  399. package/src/zig/build.zig.zon +19 -0
  400. package/src/zig/edit-buffer.zig +900 -0
  401. package/src/zig/editor-view.zig +812 -0
  402. package/src/zig/event-bus.zig +28 -0
  403. package/src/zig/event-emitter.zig +65 -0
  404. package/src/zig/file-logger.zig +92 -0
  405. package/src/zig/grapheme.zig +838 -0
  406. package/src/zig/handles.zig +267 -0
  407. package/src/zig/lib.zig +2813 -0
  408. package/src/zig/link.zig +349 -0
  409. package/src/zig/logger.zig +43 -0
  410. package/src/zig/mem-registry.zig +126 -0
  411. package/src/zig/miniaudio_shim.c +21 -0
  412. package/src/zig/native-renderable.zig +112 -0
  413. package/src/zig/native-span-feed-bench-lib.zig +7 -0
  414. package/src/zig/native-span-feed.zig +747 -0
  415. package/src/zig/renderer-output.zig +677 -0
  416. package/src/zig/renderer.zig +2106 -0
  417. package/src/zig/rope.zig +1220 -0
  418. package/src/zig/split-scrollback.zig +88 -0
  419. package/src/zig/syntax-style.zig +174 -0
  420. package/src/zig/terminal.zig +1652 -0
  421. package/src/zig/test.zig +82 -0
  422. package/src/zig/tests/README.md +18 -0
  423. package/src/zig/tests/ansi_test.zig +18 -0
  424. package/src/zig/tests/audio_test.zig +439 -0
  425. package/src/zig/tests/buffer-methods_test.zig +1111 -0
  426. package/src/zig/tests/buffer_test.zig +2926 -0
  427. package/src/zig/tests/edit-buffer-history_test.zig +299 -0
  428. package/src/zig/tests/edit-buffer_test.zig +1688 -0
  429. package/src/zig/tests/editor-view_test.zig +3454 -0
  430. package/src/zig/tests/event-emitter_test.zig +249 -0
  431. package/src/zig/tests/grapheme_test.zig +1329 -0
  432. package/src/zig/tests/handles_test.zig +123 -0
  433. package/src/zig/tests/link_test.zig +220 -0
  434. package/src/zig/tests/mem-registry_test.zig +473 -0
  435. package/src/zig/tests/memory_leak_regression_test.zig +234 -0
  436. package/src/zig/tests/native-renderable_test.zig +16 -0
  437. package/src/zig/tests/native-span-feed_test.zig +1266 -0
  438. package/src/zig/tests/renderer_test.zig +2604 -0
  439. package/src/zig/tests/rope-nested_test.zig +712 -0
  440. package/src/zig/tests/rope_fuzz_test.zig +238 -0
  441. package/src/zig/tests/rope_test.zig +2362 -0
  442. package/src/zig/tests/segment-merge.test.zig +148 -0
  443. package/src/zig/tests/split-scrollback_test.zig +70 -0
  444. package/src/zig/tests/syntax-style_test.zig +558 -0
  445. package/src/zig/tests/terminal_test.zig +1489 -0
  446. package/src/zig/tests/test-renderer.zig +126 -0
  447. package/src/zig/tests/text-buffer-drawing_test.zig +3291 -0
  448. package/src/zig/tests/text-buffer-highlights_test.zig +706 -0
  449. package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
  450. package/src/zig/tests/text-buffer-segment_test.zig +320 -0
  451. package/src/zig/tests/text-buffer-selection_test.zig +1056 -0
  452. package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
  453. package/src/zig/tests/text-buffer-view_test.zig +3672 -0
  454. package/src/zig/tests/text-buffer_test.zig +2191 -0
  455. package/src/zig/tests/unicode-width-map.zon +3909 -0
  456. package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
  457. package/src/zig/tests/utf8_test.zig +4060 -0
  458. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
  459. package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
  460. package/src/zig/tests/word-wrap-editing_test.zig +498 -0
  461. package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
  462. package/src/zig/tests/yoga_test.zig +65 -0
  463. package/src/zig/text-buffer-iterators.zig +496 -0
  464. package/src/zig/text-buffer-segment.zig +403 -0
  465. package/src/zig/text-buffer-view.zig +1428 -0
  466. package/src/zig/text-buffer.zig +1272 -0
  467. package/src/zig/utf8.zig +1954 -0
  468. package/src/zig/utils.zig +12 -0
  469. package/src/zig/uucode-stub.zig +76 -0
  470. package/src/zig/vendor/miniaudio/miniaudio.h +93468 -0
  471. package/src/zig/yoga.zig +658 -0
  472. package/src/zig-structs.ts +381 -0
  473. package/src/zig.ts +5260 -0
  474. package/tsconfig.build.json +17 -0
  475. package/tsconfig.json +27 -0
  476. package/tsconfig.node-test.json +138 -0
@@ -0,0 +1,1489 @@
1
+ const std = @import("std");
2
+ const builtin = @import("builtin");
3
+ const testing = std.testing;
4
+ const ansi = @import("../ansi.zig");
5
+ const Terminal = @import("../terminal.zig");
6
+ const utf8 = @import("../utf8.zig");
7
+
8
+ test "parseXtversion - kitty format" {
9
+ var term = Terminal.init(.{});
10
+ const response = "\x1bP>|kitty(0.40.1)\x1b\\";
11
+ term.processCapabilityResponse(response);
12
+
13
+ try testing.expectEqualStrings("kitty", term.getTerminalName());
14
+ try testing.expectEqualStrings("0.40.1", term.getTerminalVersion());
15
+ try testing.expect(term.term_info.from_xtversion);
16
+ try testing.expect(term.caps.osc52);
17
+ }
18
+
19
+ test "parseXtversion - ghostty format" {
20
+ var term = Terminal.init(.{});
21
+ const response = "\x1bP>|ghostty 1.1.3\x1b\\";
22
+ term.processCapabilityResponse(response);
23
+
24
+ try testing.expectEqualStrings("ghostty", term.getTerminalName());
25
+ try testing.expectEqualStrings("1.1.3", term.getTerminalVersion());
26
+ try testing.expect(term.term_info.from_xtversion);
27
+ try testing.expect(term.caps.osc52);
28
+ }
29
+
30
+ test "parseXtversion - tmux format" {
31
+ var term = Terminal.init(.{});
32
+ const response = "\x1bP>|tmux 3.5a\x1b\\";
33
+ term.processCapabilityResponse(response);
34
+
35
+ try testing.expectEqualStrings("tmux", term.getTerminalName());
36
+ try testing.expectEqualStrings("3.5a", term.getTerminalVersion());
37
+ try testing.expect(term.term_info.from_xtversion);
38
+ try testing.expectEqual(Terminal.Multiplexer.tmux, term.multiplexer);
39
+ try testing.expect(term.caps.osc52);
40
+ }
41
+
42
+ test "parseXtversion - with prefix data" {
43
+ var term = Terminal.init(.{});
44
+ const response = "\x1b[1;1R\x1bP>|tmux 3.5a\x1b\\";
45
+ term.processCapabilityResponse(response);
46
+
47
+ try testing.expectEqualStrings("tmux", term.getTerminalName());
48
+ try testing.expectEqualStrings("3.5a", term.getTerminalVersion());
49
+ try testing.expect(term.term_info.from_xtversion);
50
+ }
51
+
52
+ test "parseXtversion - full kitty response" {
53
+ var term = Terminal.init(.{});
54
+ const response = "\x1b[?1016;2$y\x1b[?2027;0$y\x1b[?2031;2$y\x1b[?1004;1$y\x1b[?2026;2$y\x1b[1;2R\x1b[1;3R\x1bP>|kitty(0.40.1)\x1b\\\x1b[?0u\x1b_Gi=1;EINVAL:Zero width/height not allowed\x1b\\\x1b[?62;c";
55
+ term.processCapabilityResponse(response);
56
+
57
+ try testing.expectEqualStrings("kitty", term.getTerminalName());
58
+ try testing.expectEqualStrings("0.40.1", term.getTerminalVersion());
59
+ try testing.expect(term.term_info.from_xtversion);
60
+ try testing.expect(term.caps.kitty_keyboard);
61
+ try testing.expect(term.caps.kitty_graphics);
62
+ try testing.expect(term.caps.osc52);
63
+ }
64
+
65
+ test "parseXtversion - full ghostty response" {
66
+ var term = Terminal.init(.{});
67
+ const response = "\x1b[?1016;1$y\x1b[?2027;1$y\x1b[?2031;2$y\x1b[?1004;1$y\x1b[?2004;2$y\x1b[?2026;2$y\x1b[1;1R\x1b[1;1R\x1bP>|ghostty 1.1.3\x1b\\\x1b[?0u\x1b_Gi=1;OK\x1b\\\x1b[?62;22c";
68
+ term.processCapabilityResponse(response);
69
+
70
+ try testing.expectEqualStrings("ghostty", term.getTerminalName());
71
+ try testing.expectEqualStrings("1.1.3", term.getTerminalVersion());
72
+ try testing.expect(term.term_info.from_xtversion);
73
+ }
74
+
75
+ test "notifications - OSC99 query response enables OSC99 protocol" {
76
+ var term = Terminal.init(.{});
77
+
78
+ term.processCapabilityResponse("\x1b]99;i=opentui-notifications:p=?;p=title,body:o=always:u=0,1,2\x1b\\");
79
+
80
+ try testing.expect(term.caps.notifications);
81
+ try testing.expectEqual(Terminal.NotificationProtocol.osc99, term.notification_protocol);
82
+ }
83
+
84
+ test "notifications - OSC99 query response ignores mismatched identifier" {
85
+ var term = Terminal.init(.{ .remote_mode = .remote });
86
+
87
+ term.processCapabilityResponse("\x1b]99;i=other:p=?;p=title,body\x1b\\");
88
+
89
+ try testing.expect(!term.caps.notifications);
90
+ try testing.expectEqual(Terminal.NotificationProtocol.none, term.notification_protocol);
91
+ }
92
+
93
+ test "notifications - iTerm2 feature reporting enables OSC9 protocol" {
94
+ var term = Terminal.init(.{});
95
+
96
+ term.processCapabilityResponse("\x1b]1337;Capabilities=T2NoH\x1b\\");
97
+
98
+ try testing.expect(term.caps.notifications);
99
+ try testing.expectEqual(Terminal.NotificationProtocol.osc9, term.notification_protocol);
100
+ }
101
+
102
+ test "notifications - iTerm2 feature reporting without No leaves disabled" {
103
+ var term = Terminal.init(.{ .remote_mode = .remote });
104
+
105
+ term.processCapabilityResponse("\x1b]1337;Capabilities=T2H\x1b\\");
106
+
107
+ try testing.expect(!term.caps.notifications);
108
+ try testing.expectEqual(Terminal.NotificationProtocol.none, term.notification_protocol);
109
+ }
110
+
111
+ test "notifications - xtversion heuristics prefer documented protocols" {
112
+ var kitty = Terminal.init(.{});
113
+ kitty.processCapabilityResponse("\x1bP>|kitty(0.46.2)\x1b\\");
114
+ try testing.expect(kitty.caps.notifications);
115
+ try testing.expectEqual(Terminal.NotificationProtocol.osc99, kitty.notification_protocol);
116
+
117
+ var ghostty = Terminal.init(.{});
118
+ ghostty.processCapabilityResponse("\x1bP>|ghostty 1.3.1\x1b\\");
119
+ try testing.expect(ghostty.caps.notifications);
120
+ try testing.expectEqual(Terminal.NotificationProtocol.osc777, ghostty.notification_protocol);
121
+
122
+ var iterm = Terminal.init(.{});
123
+ iterm.processCapabilityResponse("\x1bP>|iTerm2 3.6.9\x1b\\");
124
+ try testing.expect(iterm.caps.notifications);
125
+ try testing.expectEqual(Terminal.NotificationProtocol.osc9, iterm.notification_protocol);
126
+ }
127
+
128
+ test "notifications - TERM_FEATURES enables OSC9 protocol" {
129
+ var env = std.process.EnvMap.init(testing.allocator);
130
+ defer env.deinit();
131
+ try env.put("TERM_FEATURES", "T2NoH");
132
+
133
+ const term = Terminal.init(.{ .env_map = &env });
134
+
135
+ try testing.expect(term.caps.notifications);
136
+ try testing.expectEqual(Terminal.NotificationProtocol.osc9, term.notification_protocol);
137
+ }
138
+
139
+ test "notifications - Zellij env suppresses inherited host notification heuristics" {
140
+ var env = std.process.EnvMap.init(testing.allocator);
141
+ defer env.deinit();
142
+ try env.put("ZELLIJ", "0");
143
+ try env.put("ZELLIJ_PANE_ID", "1");
144
+ try env.put("TERM", "xterm-256color");
145
+ try env.put("TERM_PROGRAM", "ghostty");
146
+ try env.put("WT_SESSION", "outer-windows-terminal-session");
147
+ try env.put("TERM_FEATURES", "T2NoH");
148
+
149
+ var term = Terminal.init(.{ .env_map = &env });
150
+
151
+ try testing.expectEqual(Terminal.Multiplexer.zellij, term.multiplexer);
152
+ try testing.expectEqualStrings("Zellij", term.getTerminalName());
153
+ try testing.expect(!term.caps.notifications);
154
+ try testing.expectEqual(Terminal.NotificationProtocol.none, term.notification_protocol);
155
+
156
+ term.processCapabilityResponse("\x1b]1337;Capabilities=T2NoH\x1b\\");
157
+ try testing.expect(!term.caps.notifications);
158
+ try testing.expectEqual(Terminal.NotificationProtocol.none, term.notification_protocol);
159
+
160
+ term.processCapabilityResponse("\x1b]99;i=opentui-notifications:p=?;p=title,body:o=always\x1b\\");
161
+ try testing.expect(term.caps.notifications);
162
+ try testing.expectEqual(Terminal.NotificationProtocol.osc99, term.notification_protocol);
163
+ }
164
+
165
+ test "notifications - Zellij XTVERSION overrides inherited tmux and clears heuristics" {
166
+ var env = std.process.EnvMap.init(testing.allocator);
167
+ defer env.deinit();
168
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
169
+ try env.put("TERM", "screen-256color");
170
+ try env.put("TERM_PROGRAM", "ghostty");
171
+
172
+ var term = Terminal.init(.{ .env_map = &env });
173
+ try testing.expectEqual(Terminal.Multiplexer.tmux, term.multiplexer);
174
+ try testing.expect(term.caps.notifications);
175
+ try testing.expectEqual(Terminal.NotificationProtocol.osc777, term.notification_protocol);
176
+
177
+ term.processCapabilityResponse("\x1bP>|Zellij(0.44.1)\x1b\\");
178
+ try testing.expectEqual(Terminal.Multiplexer.zellij, term.multiplexer);
179
+ try testing.expectEqualStrings("Zellij", term.getTerminalName());
180
+ try testing.expectEqualStrings("0.44.1", term.getTerminalVersion());
181
+ try testing.expect(!term.caps.notifications);
182
+ try testing.expectEqual(Terminal.NotificationProtocol.none, term.notification_protocol);
183
+
184
+ term.processCapabilityResponse("\x1b]99;i=opentui-notifications:p=?;p=title,body:o=always\x1b\\");
185
+ try testing.expect(term.caps.notifications);
186
+ try testing.expectEqual(Terminal.NotificationProtocol.osc99, term.notification_protocol);
187
+ }
188
+
189
+ test "notifications - explicit protocol override works in tmux" {
190
+ var env = std.process.EnvMap.init(testing.allocator);
191
+ defer env.deinit();
192
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
193
+ try env.put("TERM", "screen-256color");
194
+ try env.put("OPENTUI_NOTIFICATION_PROTOCOL", "osc9");
195
+
196
+ const term = Terminal.init(.{ .env_map = &env });
197
+
198
+ try testing.expectEqual(Terminal.Multiplexer.tmux, term.multiplexer);
199
+ try testing.expect(term.caps.notifications);
200
+ try testing.expectEqual(Terminal.NotificationProtocol.osc9, term.notification_protocol);
201
+ }
202
+
203
+ test "notifications - explicit disable blocks later queries" {
204
+ var env = std.process.EnvMap.init(testing.allocator);
205
+ defer env.deinit();
206
+ try env.put("OPENTUI_NOTIFICATION_PROTOCOL", "none");
207
+
208
+ var term = Terminal.init(.{ .env_map = &env });
209
+ try testing.expect(!term.caps.notifications);
210
+ try testing.expectEqual(Terminal.NotificationProtocol.none, term.notification_protocol);
211
+
212
+ term.processCapabilityResponse("\x1b]99;i=opentui-notifications:p=?;p=title,body:o=always\x1b\\");
213
+ try testing.expect(!term.caps.notifications);
214
+ try testing.expectEqual(Terminal.NotificationProtocol.none, term.notification_protocol);
215
+ }
216
+
217
+ test "remote detection - auto mode detects SSH environment" {
218
+ var env = std.process.EnvMap.init(testing.allocator);
219
+ defer env.deinit();
220
+ try env.put("SSH_CONNECTION", "192.0.2.1 54231 192.0.2.2 22");
221
+
222
+ const term = Terminal.init(.{ .remote_mode = .auto, .env_map = &env });
223
+
224
+ try testing.expect(term.remote);
225
+ try testing.expect(term.caps.remote);
226
+ }
227
+
228
+ test "remote detection - auto mode detects mosh environment" {
229
+ var env = std.process.EnvMap.init(testing.allocator);
230
+ defer env.deinit();
231
+ try env.put("MOSH_CONNECTION", "192.0.2.1 60001");
232
+
233
+ const term = Terminal.init(.{ .remote_mode = .auto, .env_map = &env });
234
+
235
+ try testing.expect(term.remote);
236
+ try testing.expect(term.caps.remote);
237
+ }
238
+
239
+ test "remote detection - explicit local mode ignores SSH environment" {
240
+ var env = std.process.EnvMap.init(testing.allocator);
241
+ defer env.deinit();
242
+ try env.put("SSH_TTY", "/dev/pts/1");
243
+
244
+ const term = Terminal.init(.{ .remote_mode = .local, .env_map = &env });
245
+
246
+ try testing.expect(!term.remote);
247
+ try testing.expect(!term.caps.remote);
248
+ }
249
+
250
+ test "processCapabilityResponse captures startup cursor report before home probes" {
251
+ var term = Terminal.init(.{});
252
+ term.startup_cursor_query_pending = true;
253
+ term.startup_cursor_query_captured = false;
254
+
255
+ term.processCapabilityResponse("\x1b[7;11R\x1b[1;2R\x1b[1;3R");
256
+
257
+ const cursor = term.getCursorPosition();
258
+ try testing.expectEqual(@as(u32, 11), cursor.x);
259
+ try testing.expectEqual(@as(u32, 7), cursor.y);
260
+ try testing.expect(term.startup_cursor_query_captured);
261
+ try testing.expect(!term.startup_cursor_query_pending);
262
+ try testing.expect(term.caps.explicit_width);
263
+ try testing.expect(term.caps.scaled_text);
264
+ }
265
+
266
+ test "environment variables - should be overridden by xtversion" {
267
+ var term = Terminal.init(.{});
268
+
269
+ // First check environment (simulated by setting values directly)
270
+ term.term_info.name_len = 6;
271
+ @memcpy(term.term_info.name[0..6], "vscode");
272
+ term.term_info.version_len = 5;
273
+ @memcpy(term.term_info.version[0..5], "1.0.0");
274
+ term.term_info.from_xtversion = false;
275
+
276
+ try testing.expectEqualStrings("vscode", term.getTerminalName());
277
+ try testing.expectEqualStrings("1.0.0", term.getTerminalVersion());
278
+ try testing.expect(!term.term_info.from_xtversion);
279
+
280
+ // Now process xtversion response - should override
281
+ const response = "\x1bP>|kitty(0.40.1)\x1b\\";
282
+ term.processCapabilityResponse(response);
283
+
284
+ try testing.expectEqualStrings("kitty", term.getTerminalName());
285
+ try testing.expectEqualStrings("0.40.1", term.getTerminalVersion());
286
+ try testing.expect(term.term_info.from_xtversion);
287
+ }
288
+
289
+ test "remote without forwarded env map ignores local env overrides" {
290
+ const term = Terminal.init(.{ .remote_mode = .remote });
291
+
292
+ try testing.expect(term.remote);
293
+ try testing.expect(term.caps.remote);
294
+ try testing.expectEqual(Terminal.Multiplexer.none, term.multiplexer);
295
+ try testing.expect(!term.caps.osc52);
296
+ try testing.expect(!term.caps.explicit_cursor_positioning);
297
+ }
298
+
299
+ test "TERM_PROGRAM tmux provides initial tmux version before xtversion" {
300
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
301
+
302
+ var env = std.process.EnvMap.init(testing.allocator);
303
+ defer env.deinit();
304
+ try env.put("TERM_PROGRAM", "tmux");
305
+ try env.put("TERM_PROGRAM_VERSION", "3.6a");
306
+ try env.put("TERM", "xterm-256color");
307
+
308
+ var term = Terminal.init(.{ .env_map = &env });
309
+
310
+ try testing.expectEqual(Terminal.Multiplexer.tmux, term.multiplexer);
311
+ try testing.expectEqualStrings("tmux", term.getTerminalName());
312
+ try testing.expectEqualStrings("3.6a", term.getTerminalVersion());
313
+ try testing.expect(!term.term_info.from_xtversion);
314
+ }
315
+
316
+ test "remote applies forwarded env overrides and capability responses" {
317
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
318
+
319
+ var env = std.process.EnvMap.init(testing.allocator);
320
+ defer env.deinit();
321
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
322
+ try env.put("TERM", "screen-256color");
323
+ try env.put("TERM_PROGRAM", "iTerm.app");
324
+ try env.put("WT_SESSION", "test-session");
325
+
326
+ var term = Terminal.init(.{ .remote_mode = .remote, .env_map = &env });
327
+
328
+ try testing.expect(term.remote);
329
+ try testing.expect(term.caps.remote);
330
+ try testing.expectEqual(Terminal.Multiplexer.tmux, term.multiplexer);
331
+ try testing.expect(term.caps.osc52);
332
+ try testing.expect(term.caps.explicit_cursor_positioning);
333
+ try testing.expect(term.caps.ansi256);
334
+
335
+ term.processCapabilityResponse("\x1bP>|kitty(0.40.1)\x1b\\");
336
+ try testing.expect(term.caps.rgb);
337
+ try testing.expect(term.caps.osc52);
338
+ }
339
+
340
+ test "setHostEnvVar applies env overrides in shared library mode" {
341
+ var term = Terminal.init(.{});
342
+ defer term.deinit();
343
+
344
+ try term.setHostEnvVar(testing.allocator, "TERM", "screen");
345
+ try testing.expectEqual(Terminal.Multiplexer.screen, term.multiplexer);
346
+ try testing.expect(term.skip_graphics_query);
347
+ try testing.expect(term.caps.unicode == .wcwidth);
348
+ try testing.expect(term.caps.explicit_cursor_positioning);
349
+
350
+ try term.setHostEnvVar(testing.allocator, "OPENTUI_FORCE_UNICODE", "1");
351
+ try testing.expect(term.caps.unicode == .unicode);
352
+
353
+ try term.setHostEnvVar(testing.allocator, "OPENTUI_GRAPHICS", "0");
354
+ try testing.expect(term.skip_graphics_query);
355
+ }
356
+
357
+ test "environment overrides - enables hyperlinks for WSL Windows Terminal xterm" {
358
+ var env = std.process.EnvMap.init(testing.allocator);
359
+ defer env.deinit();
360
+ try env.put("WSL_DISTRO_NAME", "Ubuntu");
361
+ try env.put("WT_SESSION", "test-session");
362
+ try env.put("TERM", "xterm-256color");
363
+
364
+ const term = Terminal.init(.{ .env_map = &env });
365
+
366
+ try testing.expect(term.caps.hyperlinks);
367
+ }
368
+
369
+ test "environment overrides - does not enable hyperlinks for WSL without WT_SESSION" {
370
+ var env = std.process.EnvMap.init(testing.allocator);
371
+ defer env.deinit();
372
+ try env.put("WSL_DISTRO_NAME", "Ubuntu");
373
+ try env.put("TERM", "xterm-256color");
374
+
375
+ const term = Terminal.init(.{ .env_map = &env });
376
+
377
+ try testing.expect(!term.caps.hyperlinks);
378
+ }
379
+
380
+ test "environment overrides - does not enable hyperlinks for WSL non-xterm terms" {
381
+ var env = std.process.EnvMap.init(testing.allocator);
382
+ defer env.deinit();
383
+ try env.put("WSL_INTEROP", "/run/WSL/123_interop");
384
+ try env.put("WT_SESSION", "test-session");
385
+ try env.put("TERM", "screen-256color");
386
+
387
+ const term = Terminal.init(.{ .env_map = &env });
388
+
389
+ try testing.expect(!term.caps.hyperlinks);
390
+ }
391
+
392
+ test "setHostEnvVar detects ansi256 separately from rgb" {
393
+ var env = std.process.EnvMap.init(testing.allocator);
394
+ defer env.deinit();
395
+ try env.put("TERM", "screen-256color");
396
+
397
+ var term = Terminal.init(.{ .env_map = &env });
398
+ defer term.deinit();
399
+
400
+ try testing.expect(term.caps.ansi256);
401
+ try testing.expect(!term.caps.rgb);
402
+
403
+ try term.setHostEnvVar(testing.allocator, "COLORTERM", "truecolor");
404
+ try testing.expect(term.caps.rgb);
405
+ try testing.expect(term.caps.ansi256);
406
+ }
407
+
408
+ test "environment overrides - WT_SESSION enables rgb and ansi256" {
409
+ var env = std.process.EnvMap.init(testing.allocator);
410
+ defer env.deinit();
411
+ try env.put("TERM", "xterm-256color");
412
+ try env.put("WT_SESSION", "test-session");
413
+
414
+ const term = Terminal.init(.{ .env_map = &env });
415
+
416
+ try testing.expect(term.caps.rgb);
417
+ try testing.expect(term.caps.ansi256);
418
+ }
419
+
420
+ test "parseXtversion - terminal name only" {
421
+ var term = Terminal.init(.{});
422
+ const response = "\x1bP>|wezterm\x1b\\";
423
+ term.processCapabilityResponse(response);
424
+
425
+ try testing.expectEqualStrings("wezterm", term.getTerminalName());
426
+ try testing.expectEqualStrings("", term.getTerminalVersion());
427
+ try testing.expect(term.term_info.from_xtversion);
428
+ try testing.expect(term.caps.osc52);
429
+ }
430
+
431
+ test "parseXtversion - empty response" {
432
+ var term = Terminal.init(.{});
433
+
434
+ const initial_name_len = term.term_info.name_len;
435
+ const initial_version_len = term.term_info.version_len;
436
+
437
+ const response = "\x1bP>|\x1b\\";
438
+ term.processCapabilityResponse(response);
439
+
440
+ try testing.expectEqual(initial_name_len, term.term_info.name_len);
441
+ try testing.expectEqual(initial_version_len, term.term_info.version_len);
442
+ }
443
+
444
+ // Test buffer for capturing terminal output
445
+ const TestWriter = struct {
446
+ buffer: std.ArrayListUnmanaged(u8),
447
+ allocator: std.mem.Allocator,
448
+
449
+ pub fn init(allocator: std.mem.Allocator) TestWriter {
450
+ return .{ .buffer = .{}, .allocator = allocator };
451
+ }
452
+
453
+ pub fn deinit(self: *TestWriter) void {
454
+ self.buffer.deinit(self.allocator);
455
+ self.* = undefined;
456
+ }
457
+
458
+ pub fn writeAll(self: *TestWriter, data: []const u8) !void {
459
+ try self.buffer.appendSlice(self.allocator, data);
460
+ }
461
+
462
+ pub fn writeByte(self: *TestWriter, byte: u8) !void {
463
+ try self.buffer.append(self.allocator, byte);
464
+ }
465
+
466
+ pub fn print(self: *TestWriter, comptime fmt: []const u8, args: anytype) !void {
467
+ try self.buffer.writer(self.allocator).print(fmt, args);
468
+ }
469
+
470
+ pub fn getWritten(self: *TestWriter) []const u8 {
471
+ return self.buffer.items;
472
+ }
473
+
474
+ pub fn reset(self: *TestWriter) void {
475
+ self.buffer.clearRetainingCapacity();
476
+ }
477
+ };
478
+
479
+ test "queryTerminalSend - sends unwrapped queries when not in tmux" {
480
+ // Note: This test may fail if running inside tmux since checkEnvironmentOverrides
481
+ // reads TMUX/TERM env vars. We test the logic directly instead.
482
+ var term = Terminal.init(.{});
483
+
484
+ // Skip test if actually running in tmux
485
+ if (term.isInTmux()) return error.SkipZigTest;
486
+
487
+ var writer = TestWriter.init(testing.allocator);
488
+ defer writer.deinit();
489
+
490
+ try term.queryTerminalSend(&writer);
491
+
492
+ const output = writer.getWritten();
493
+
494
+ const idx_osc_theme_queries = std.mem.indexOf(u8, output, ansi.ANSI.oscThemeQueries).?;
495
+ const idx_xtversion = std.mem.indexOf(u8, output, "\x1b[>0q").?;
496
+
497
+ // Should contain xtversion
498
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[>0q") != null);
499
+ try testing.expect(idx_osc_theme_queries < idx_xtversion);
500
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[?996n") == null);
501
+
502
+ // Should contain unwrapped DECRQM queries (single ESC)
503
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[?1016$p") != null);
504
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[?2027$p") != null);
505
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[?u") != null);
506
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bP+q4d73\x1b\\") != null);
507
+
508
+ // Should NOT contain tmux DCS wrapper
509
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;") == null);
510
+
511
+ // Should mark capability queries as pending
512
+ try testing.expect(term.capability_queries_pending);
513
+ }
514
+
515
+ test "queryTerminalSend - sends DCS wrapped queries when in tmux" {
516
+ // Note: This test checks logic when tmux is detected.
517
+ // We can't easily force tmux detection since checkEnvironmentOverrides resets it,
518
+ // so we test this via sendPendingQueries tests instead.
519
+ var term = Terminal.init(.{});
520
+
521
+ // Only run the DCS wrapping test if actually in tmux
522
+ if (!term.isInTmux()) return error.SkipZigTest;
523
+
524
+ var writer = TestWriter.init(testing.allocator);
525
+ defer writer.deinit();
526
+
527
+ try term.queryTerminalSend(&writer);
528
+
529
+ const output = writer.getWritten();
530
+
531
+ const idx_osc_theme_queries = std.mem.indexOf(u8, output, ansi.ANSI.oscThemeQueries).?;
532
+ const idx_xtversion = std.mem.indexOf(u8, output, "\x1b[>0q").?;
533
+
534
+ // Should contain xtversion (unwrapped - used for detection)
535
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[>0q") != null);
536
+ try testing.expect(idx_osc_theme_queries < idx_xtversion);
537
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[?996n") == null);
538
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;\x1b\x1b]10;?") == null);
539
+
540
+ // Should contain tmux DCS wrapper start and doubled ESC for queries
541
+ // wrapForTmux wraps all queries together with one DCS envelope
542
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;\x1b\x1bP+q4d73\x1b\x1b\\") != null);
543
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b\x1b[?1016$p") != null);
544
+
545
+ // Should NOT mark capability queries as pending (already sent wrapped)
546
+ try testing.expect(!term.capability_queries_pending);
547
+ }
548
+
549
+ test "queryTerminalSend - sends plain theme queries when TMUX is set" {
550
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
551
+
552
+ var env = std.process.EnvMap.init(testing.allocator);
553
+ defer env.deinit();
554
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
555
+ try env.put("TERM", "screen-256color");
556
+
557
+ var term = Terminal.init(.{ .env_map = &env });
558
+ var writer = TestWriter.init(testing.allocator);
559
+ defer writer.deinit();
560
+
561
+ try term.queryTerminalSend(&writer);
562
+
563
+ const output = writer.getWritten();
564
+
565
+ try testing.expectEqual(Terminal.Multiplexer.tmux, term.multiplexer);
566
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.oscThemeQueries) != null);
567
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;\x1b\x1b]10;?") == null);
568
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[?996n") == null);
569
+ }
570
+
571
+ test "sendPendingQueries - sends wrapped queries after tmux detected via xtversion" {
572
+ var term = Terminal.init(.{});
573
+ term.multiplexer = .none;
574
+ term.capability_queries_pending = true;
575
+ term.graphics_query_pending = true;
576
+
577
+ // Simulate tmux detected via xtversion
578
+ term.processCapabilityResponse("\x1bP>|tmux 3.5a\x1b\\");
579
+
580
+ var writer = TestWriter.init(testing.allocator);
581
+ defer writer.deinit();
582
+
583
+ const did_send = try term.sendPendingQueries(&writer);
584
+
585
+ try testing.expect(did_send);
586
+
587
+ const output = writer.getWritten();
588
+
589
+ // Should send DCS wrapped capability queries (wrapForTmux wraps all queries together)
590
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;\x1b\x1bP+q4d73\x1b\x1b\\") != null);
591
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b\x1b[?1016$p") != null);
592
+
593
+ // Should send DCS wrapped graphics query
594
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;\x1b\x1b_G") != null);
595
+
596
+ // Should clear pending flags
597
+ try testing.expect(!term.capability_queries_pending);
598
+ try testing.expect(!term.graphics_query_pending);
599
+ }
600
+
601
+ test "sendPendingQueries - sends unwrapped graphics query for non-tmux terminal" {
602
+ var term = Terminal.init(.{});
603
+ term.multiplexer = .none;
604
+ term.capability_queries_pending = true;
605
+ term.graphics_query_pending = true;
606
+
607
+ // Simulate non-tmux terminal detected via xtversion
608
+ term.term_info.from_xtversion = true;
609
+ term.term_info.name_len = 5;
610
+ @memcpy(term.term_info.name[0..5], "kitty");
611
+
612
+ var writer = TestWriter.init(testing.allocator);
613
+ defer writer.deinit();
614
+
615
+ const did_send = try term.sendPendingQueries(&writer);
616
+
617
+ try testing.expect(did_send);
618
+
619
+ const output = writer.getWritten();
620
+
621
+ // Should NOT send DCS wrapped capability queries (not tmux)
622
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;") == null);
623
+
624
+ // Should send unwrapped graphics query
625
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b_Gi=31337") != null);
626
+
627
+ // Should clear pending flags
628
+ try testing.expect(!term.capability_queries_pending);
629
+ try testing.expect(!term.graphics_query_pending);
630
+ }
631
+
632
+ test "sendPendingQueries - sends unwrapped graphics query even without xtversion response" {
633
+ // This covers terminals that support kitty graphics but don't respond to xtversion.
634
+ // The graphics query should still be sent (unwrapped) so we can detect graphics support.
635
+ var term = Terminal.init(.{});
636
+ term.multiplexer = .none;
637
+ term.term_info.from_xtversion = false;
638
+ term.capability_queries_pending = true;
639
+ term.graphics_query_pending = true;
640
+
641
+ var writer = TestWriter.init(testing.allocator);
642
+ defer writer.deinit();
643
+
644
+ const did_send = try term.sendPendingQueries(&writer);
645
+
646
+ try testing.expect(did_send);
647
+
648
+ const output = writer.getWritten();
649
+
650
+ // Should send unwrapped graphics query (not tmux, so no DCS wrapper)
651
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b_Gi=31337") != null);
652
+ try testing.expect(std.mem.indexOf(u8, output, "\x1bPtmux;") == null);
653
+
654
+ // Should clear graphics pending flag
655
+ try testing.expect(!term.graphics_query_pending);
656
+
657
+ // Capability queries should NOT be re-sent (no xtversion means we don't know if tmux,
658
+ // but they were already sent unwrapped in queryTerminalSend)
659
+ try testing.expect(!term.capability_queries_pending);
660
+ }
661
+
662
+ test "sendPendingQueries - skips graphics when skip_graphics_query is set" {
663
+ var term = Terminal.init(.{});
664
+ term.multiplexer = .tmux;
665
+ term.skip_graphics_query = true;
666
+ term.graphics_query_pending = true;
667
+ term.capability_queries_pending = false;
668
+
669
+ var writer = TestWriter.init(testing.allocator);
670
+ defer writer.deinit();
671
+
672
+ const did_send = try term.sendPendingQueries(&writer);
673
+
674
+ try testing.expect(!did_send);
675
+
676
+ const output = writer.getWritten();
677
+ try testing.expect(std.mem.indexOf(u8, output, "Gi=31337") == null);
678
+ }
679
+
680
+ test "isXtversionTmux - detects tmux from xtversion" {
681
+ var term = Terminal.init(.{});
682
+
683
+ // Not from xtversion
684
+ term.term_info.from_xtversion = false;
685
+ term.term_info.name_len = 4;
686
+ @memcpy(term.term_info.name[0..4], "tmux");
687
+ try testing.expect(!term.isXtversionTmux());
688
+
689
+ // From xtversion but not tmux
690
+ term.term_info.from_xtversion = true;
691
+ term.term_info.name_len = 5;
692
+ @memcpy(term.term_info.name[0..5], "kitty");
693
+ try testing.expect(!term.isXtversionTmux());
694
+
695
+ // From xtversion and is tmux
696
+ term.term_info.name_len = 4;
697
+ @memcpy(term.term_info.name[0..4], "tmux");
698
+ try testing.expect(term.isXtversionTmux());
699
+ }
700
+
701
+ // ============================================================================
702
+ // GRAPHEME CURSOR POSITIONING CAPABILITY TESTS
703
+ // ============================================================================
704
+
705
+ test "processCapabilityResponse - tmux sets explicit_cursor_positioning" {
706
+ var term: Terminal = .{};
707
+
708
+ term.caps.explicit_cursor_positioning = false;
709
+ term.caps.unicode = .unicode;
710
+
711
+ const response = "\x1bP>|tmux 3.5a\x1b\\";
712
+ term.processCapabilityResponse(response);
713
+
714
+ try testing.expect(term.caps.explicit_cursor_positioning);
715
+ try testing.expectEqual(utf8.WidthMethod.wcwidth, term.caps.unicode);
716
+ }
717
+
718
+ test "processCapabilityResponse - alacritty sets explicit_cursor_positioning" {
719
+ var term: Terminal = .{};
720
+
721
+ term.caps.explicit_cursor_positioning = false;
722
+
723
+ const response = "\x1bP>|alacritty 0.13.0\x1b\\";
724
+ term.processCapabilityResponse(response);
725
+
726
+ try testing.expect(term.caps.explicit_cursor_positioning);
727
+ }
728
+
729
+ test "processCapabilityResponse - kitty does not set explicit_cursor_positioning" {
730
+ var term: Terminal = .{};
731
+
732
+ term.caps.explicit_cursor_positioning = false;
733
+
734
+ const response = "\x1bP>|kitty(0.40.1)\x1b\\";
735
+ term.processCapabilityResponse(response);
736
+
737
+ try testing.expect(!term.caps.explicit_cursor_positioning);
738
+ }
739
+
740
+ test "processCapabilityResponse - ghostty does not set explicit_cursor_positioning" {
741
+ var term: Terminal = .{};
742
+
743
+ term.caps.explicit_cursor_positioning = false;
744
+
745
+ const response = "\x1bP>|ghostty 1.1.3\x1b\\";
746
+ term.processCapabilityResponse(response);
747
+
748
+ try testing.expect(!term.caps.explicit_cursor_positioning);
749
+ }
750
+
751
+ test "processCapabilityResponse - wezterm applies osc52 and hyperlink heuristics" {
752
+ var term: Terminal = .{};
753
+
754
+ const response = "\x1bP>|wezterm\x1b\\";
755
+ term.processCapabilityResponse(response);
756
+
757
+ try testing.expect(!term.caps.rgb);
758
+ try testing.expect(!term.caps.ansi256);
759
+ try testing.expect(term.caps.osc52);
760
+ try testing.expect(term.caps.hyperlinks);
761
+ }
762
+
763
+ test "processCapabilityResponse - foot applies osc52 heuristic without explicit cursor positioning" {
764
+ var term: Terminal = .{};
765
+
766
+ const response = "\x1bP>|foot 1.17.2\x1b\\";
767
+ term.processCapabilityResponse(response);
768
+
769
+ try testing.expect(!term.caps.rgb);
770
+ try testing.expect(!term.caps.ansi256);
771
+ try testing.expect(term.caps.osc52);
772
+ try testing.expect(!term.caps.explicit_cursor_positioning);
773
+ }
774
+
775
+ test "processCapabilityResponse - XTGETTCAP Ms only establishes positive support" {
776
+ var supported: Terminal = .{};
777
+ supported.processCapabilityResponse("\x1bP1+r4d73=2570312573\x1b\\");
778
+ try testing.expectEqual(Terminal.Osc52Support.supported, supported.osc52_support);
779
+ try testing.expect(supported.caps.osc52);
780
+
781
+ var bare_negative: Terminal = .{};
782
+ bare_negative.processCapabilityResponse("\x1bP>|iTerm2 3.5.0\x1b\\");
783
+ bare_negative.processCapabilityResponse("\x1bP0+r\x1b\\");
784
+ try testing.expectEqual(Terminal.Osc52Support.unknown, bare_negative.osc52_support);
785
+ try testing.expect(bare_negative.caps.osc52);
786
+
787
+ var named_negative: Terminal = .{};
788
+ named_negative.processCapabilityResponse("\x1bP>|kitty(0.40.1)\x1b\\");
789
+ named_negative.processCapabilityResponse("\x1bP0+r4D73\x1b\\");
790
+ try testing.expectEqual(Terminal.Osc52Support.unknown, named_negative.osc52_support);
791
+ try testing.expect(named_negative.caps.osc52);
792
+
793
+ var unknown: Terminal = .{};
794
+ unknown.processCapabilityResponse("\x1bP1+r4d73\x1b\\");
795
+ try testing.expectEqual(Terminal.Osc52Support.unknown, unknown.osc52_support);
796
+ unknown.processCapabilityResponse("\x1bP1+r4d73=\x1b\\");
797
+ try testing.expectEqual(Terminal.Osc52Support.unknown, unknown.osc52_support);
798
+ unknown.processCapabilityResponse("\x1bP1+r4d73=abc\x1b\\");
799
+ try testing.expectEqual(Terminal.Osc52Support.unknown, unknown.osc52_support);
800
+ unknown.processCapabilityResponse("\x1bP1+r4d73=zz\x1b\\");
801
+ try testing.expectEqual(Terminal.Osc52Support.unknown, unknown.osc52_support);
802
+ unknown.processCapabilityResponse("\x1bP1+r544e=787465726d\x1b\\");
803
+ try testing.expectEqual(Terminal.Osc52Support.unknown, unknown.osc52_support);
804
+ }
805
+
806
+ // ============================================================================
807
+ // CLIPBOARD (OSC 52) TESTS
808
+ // ============================================================================
809
+
810
+ test "writeClipboard - generates basic OSC52 sequence" {
811
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
812
+
813
+ var env = std.process.EnvMap.init(testing.allocator);
814
+ defer env.deinit();
815
+
816
+ var term = Terminal.init(.{ .env_map = &env });
817
+ term.caps.osc52 = true;
818
+
819
+ var writer = TestWriter.init(testing.allocator);
820
+ defer writer.deinit();
821
+
822
+ try term.writeClipboard(&writer, .clipboard, "hello");
823
+
824
+ const output = writer.getWritten();
825
+ // Should be: ESC]52;c;aGVsbG8=ESC\
826
+ try testing.expectEqualStrings("\x1b]52;c;aGVsbG8=\x1b\\", output);
827
+ }
828
+
829
+ test "writeClipboard - maps every selection target to its OSC 52 byte" {
830
+ const cases = [_]struct {
831
+ target: Terminal.ClipboardTarget,
832
+ expected: []const u8,
833
+ }{
834
+ .{ .target = .clipboard, .expected = "\x1b]52;c;eA==\x1b\\" },
835
+ .{ .target = .primary, .expected = "\x1b]52;p;eA==\x1b\\" },
836
+ .{ .target = .select, .expected = "\x1b]52;s;eA==\x1b\\" },
837
+ .{ .target = .secondary, .expected = "\x1b]52;q;eA==\x1b\\" },
838
+ };
839
+
840
+ for (cases) |case| {
841
+ var term: Terminal = .{};
842
+ var writer = TestWriter.init(testing.allocator);
843
+ defer writer.deinit();
844
+
845
+ try term.writeClipboard(&writer, case.target, "x");
846
+ try testing.expectEqualStrings(case.expected, writer.getWritten());
847
+ }
848
+ }
849
+
850
+ test "writeNotification - returns false when unsupported" {
851
+ var term = Terminal.init(.{ .remote_mode = .remote });
852
+ var writer = TestWriter.init(testing.allocator);
853
+ defer writer.deinit();
854
+
855
+ const ok = try term.writeNotification(testing.allocator, &writer, "Hello", null);
856
+
857
+ try testing.expect(!ok);
858
+ try testing.expectEqual(@as(usize, 0), writer.getWritten().len);
859
+ }
860
+
861
+ test "writeNotification - writes OSC99 title and body with base64 payloads" {
862
+ var env = std.process.EnvMap.init(testing.allocator);
863
+ defer env.deinit();
864
+ var term = Terminal.init(.{ .env_map = &env });
865
+ term.caps.notifications = true;
866
+ term.notification_protocol = .osc99;
867
+
868
+ var writer = TestWriter.init(testing.allocator);
869
+ defer writer.deinit();
870
+
871
+ const ok = try term.writeNotification(testing.allocator, &writer, "Body", "Title");
872
+
873
+ try testing.expect(ok);
874
+ try testing.expectEqualStrings("\x1b]99;i=opentui-1:p=title:e=1:d=0;VGl0bGU=\x1b\\\x1b]99;i=opentui-1:p=body:e=1:d=1;Qm9keQ==\x1b\\", writer.getWritten());
875
+ }
876
+
877
+ test "writeNotification - writes OSC777 and sanitizes semicolons and controls" {
878
+ var env = std.process.EnvMap.init(testing.allocator);
879
+ defer env.deinit();
880
+ var term = Terminal.init(.{ .env_map = &env });
881
+ term.caps.notifications = true;
882
+ term.notification_protocol = .osc777;
883
+
884
+ var writer = TestWriter.init(testing.allocator);
885
+ defer writer.deinit();
886
+
887
+ const ok = try term.writeNotification(testing.allocator, &writer, "Bo;dy\n", "Ti;tle");
888
+
889
+ try testing.expect(ok);
890
+ try testing.expectEqualStrings("\x1b]777;notify;Ti tle;Bo dy \x1b\\", writer.getWritten());
891
+ }
892
+
893
+ test "writeNotification - writes OSC9 combined title and message" {
894
+ var env = std.process.EnvMap.init(testing.allocator);
895
+ defer env.deinit();
896
+ var term = Terminal.init(.{ .env_map = &env });
897
+ term.caps.notifications = true;
898
+ term.notification_protocol = .osc9;
899
+
900
+ var writer = TestWriter.init(testing.allocator);
901
+ defer writer.deinit();
902
+
903
+ const ok = try term.writeNotification(testing.allocator, &writer, "Body\n", "Title");
904
+
905
+ try testing.expect(ok);
906
+ try testing.expectEqualStrings("\x1b]9;Title: Body \x1b\\", writer.getWritten());
907
+ }
908
+
909
+ test "writeNotification - wraps OSC777 in tmux passthrough" {
910
+ var env = std.process.EnvMap.init(testing.allocator);
911
+ defer env.deinit();
912
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
913
+
914
+ var term = Terminal.init(.{ .env_map = &env });
915
+ term.caps.notifications = true;
916
+ term.notification_protocol = .osc777;
917
+
918
+ var writer = TestWriter.init(testing.allocator);
919
+ defer writer.deinit();
920
+
921
+ const ok = try term.writeNotification(testing.allocator, &writer, "Body", "Title");
922
+
923
+ try testing.expect(ok);
924
+ try testing.expect(std.mem.startsWith(u8, writer.getWritten(), "\x1bPtmux;"));
925
+ try testing.expect(std.mem.indexOf(u8, writer.getWritten(), "\x1b\x1b]777;notify;Title;Body") != null);
926
+ try testing.expect(std.mem.endsWith(u8, writer.getWritten(), "\x1b\\"));
927
+ }
928
+
929
+ test "writeNotification - wraps OSC99 in tmux passthrough" {
930
+ var env = std.process.EnvMap.init(testing.allocator);
931
+ defer env.deinit();
932
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
933
+
934
+ var term = Terminal.init(.{ .env_map = &env });
935
+ term.caps.notifications = true;
936
+ term.notification_protocol = .osc99;
937
+
938
+ var writer = TestWriter.init(testing.allocator);
939
+ defer writer.deinit();
940
+
941
+ const ok = try term.writeNotification(testing.allocator, &writer, "Body", null);
942
+
943
+ try testing.expect(ok);
944
+ try testing.expect(std.mem.startsWith(u8, writer.getWritten(), "\x1bPtmux;"));
945
+ try testing.expect(std.mem.indexOf(u8, writer.getWritten(), "\x1b\x1b]99;i=opentui-1:p=body:e=1:d=1;Qm9keQ==") != null);
946
+ try testing.expect(std.mem.endsWith(u8, writer.getWritten(), "\x1b\\"));
947
+ }
948
+
949
+ test "writeNotification - writes raw OSC99 in Zellij" {
950
+ var env = std.process.EnvMap.init(testing.allocator);
951
+ defer env.deinit();
952
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
953
+ try env.put("TERM_PROGRAM", "ghostty");
954
+
955
+ var term = Terminal.init(.{ .env_map = &env });
956
+ term.processCapabilityResponse("\x1bP>|Zellij(0.44.1)\x1b\\");
957
+ term.processCapabilityResponse("\x1b]99;i=opentui-notifications:p=?;p=title,body:o=always\x1b\\");
958
+
959
+ var writer = TestWriter.init(testing.allocator);
960
+ defer writer.deinit();
961
+
962
+ const ok = try term.writeNotification(testing.allocator, &writer, "Body", null);
963
+
964
+ try testing.expect(ok);
965
+ try testing.expect(!std.mem.startsWith(u8, writer.getWritten(), "\x1bPtmux;"));
966
+ try testing.expectEqualStrings("\x1b]99;i=opentui-1:p=body:e=1:d=1;Qm9keQ==\x1b\\", writer.getWritten());
967
+ }
968
+
969
+ test "writeClipboard - supports different targets" {
970
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
971
+
972
+ var env = std.process.EnvMap.init(testing.allocator);
973
+ defer env.deinit();
974
+
975
+ var term = Terminal.init(.{ .env_map = &env });
976
+ term.caps.osc52 = true;
977
+
978
+ var writer = TestWriter.init(testing.allocator);
979
+ defer writer.deinit();
980
+
981
+ try term.writeClipboard(&writer, .primary, "test");
982
+ try testing.expect(std.mem.indexOf(u8, writer.getWritten(), "\x1b]52;p;") != null);
983
+
984
+ writer.reset();
985
+ try term.writeClipboard(&writer, .select, "test");
986
+ try testing.expect(std.mem.indexOf(u8, writer.getWritten(), "\x1b]52;s;") != null);
987
+
988
+ writer.reset();
989
+ try term.writeClipboard(&writer, .secondary, "test");
990
+ try testing.expect(std.mem.indexOf(u8, writer.getWritten(), "\x1b]52;q;") != null);
991
+ }
992
+
993
+ test "writeClipboard - returns error when OSC52 not supported" {
994
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
995
+
996
+ var term = Terminal.init(.{});
997
+ term.osc52_support = .unsupported;
998
+
999
+ var writer = TestWriter.init(testing.allocator);
1000
+ defer writer.deinit();
1001
+
1002
+ const result = term.writeClipboard(&writer, .clipboard, "test");
1003
+ try testing.expectError(error.NotSupported, result);
1004
+ }
1005
+
1006
+ test "writeClipboard - emits optimistically when XTGETTCAP state is unknown" {
1007
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1008
+
1009
+ var env = std.process.EnvMap.init(testing.allocator);
1010
+ defer env.deinit();
1011
+ var term = Terminal.init(.{ .env_map = &env });
1012
+ try testing.expectEqual(Terminal.Osc52Support.unknown, term.osc52_support);
1013
+
1014
+ var writer = TestWriter.init(testing.allocator);
1015
+ defer writer.deinit();
1016
+ try term.writeClipboard(&writer, .clipboard, "test");
1017
+
1018
+ try testing.expectEqualStrings("\x1b]52;c;dGVzdA==\x1b\\", writer.getWritten());
1019
+ }
1020
+
1021
+ test "writeClipboard - writes large payload without a fixed buffer limit" {
1022
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1023
+
1024
+ var env = std.process.EnvMap.init(testing.allocator);
1025
+ defer env.deinit();
1026
+ var term = Terminal.init(.{ .env_map = &env });
1027
+
1028
+ const payload = try testing.allocator.alloc(u8, 16 * 1024);
1029
+ defer testing.allocator.free(payload);
1030
+ @memset(payload, 'A');
1031
+
1032
+ var writer = TestWriter.init(testing.allocator);
1033
+ defer writer.deinit();
1034
+ try term.writeClipboard(&writer, .clipboard, payload);
1035
+
1036
+ const encoded_len = std.base64.standard.Encoder.calcSize(payload.len);
1037
+ try testing.expectEqual(encoded_len + 9, writer.getWritten().len);
1038
+ try testing.expect(std.mem.startsWith(u8, writer.getWritten(), "\x1b]52;c;"));
1039
+ try testing.expect(std.mem.endsWith(u8, writer.getWritten(), "\x1b\\"));
1040
+ }
1041
+
1042
+ test "writeClipboard - writes large payload through tmux passthrough" {
1043
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1044
+
1045
+ var env = std.process.EnvMap.init(testing.allocator);
1046
+ defer env.deinit();
1047
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
1048
+ var term = Terminal.init(.{ .env_map = &env });
1049
+
1050
+ const payload = try testing.allocator.alloc(u8, 16 * 1024);
1051
+ defer testing.allocator.free(payload);
1052
+ @memset(payload, 'A');
1053
+
1054
+ var writer = TestWriter.init(testing.allocator);
1055
+ defer writer.deinit();
1056
+ try term.writeClipboard(&writer, .clipboard, payload);
1057
+
1058
+ const output = writer.getWritten();
1059
+ const encoded_len = std.base64.standard.Encoder.calcSize(payload.len);
1060
+ try testing.expectEqual(encoded_len + 20, output.len);
1061
+ try testing.expect(std.mem.startsWith(u8, output, "\x1bPtmux;\x1b\x1b]52;c;"));
1062
+ try testing.expect(std.mem.endsWith(u8, output, "\x1b\x1b\\\x1b\\"));
1063
+ }
1064
+
1065
+ test "writeClipboard - chunks large payload through GNU Screen passthrough" {
1066
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1067
+
1068
+ var env = std.process.EnvMap.init(testing.allocator);
1069
+ defer env.deinit();
1070
+ try env.put("STY", "12345.pts-0.hostname");
1071
+ var term = Terminal.init(.{ .env_map = &env });
1072
+
1073
+ const payload = try testing.allocator.alloc(u8, 16 * 1024);
1074
+ defer testing.allocator.free(payload);
1075
+ @memset(payload, 'A');
1076
+
1077
+ var writer = TestWriter.init(testing.allocator);
1078
+ defer writer.deinit();
1079
+ try term.writeClipboard(&writer, .clipboard, payload);
1080
+
1081
+ const output = writer.getWritten();
1082
+ try testing.expect(countSubstring(output, ansi.ANSI.screenDcsStart) > 1);
1083
+ try testing.expect(std.mem.endsWith(u8, output, ansi.ANSI.screenDcsEnd));
1084
+
1085
+ var frame_start: usize = 0;
1086
+ while (std.mem.indexOfPos(u8, output, frame_start, ansi.ANSI.screenDcsStart)) |start| {
1087
+ const content_start = start + ansi.ANSI.screenDcsStart.len;
1088
+ const next_start = std.mem.indexOfPos(u8, output, content_start, ansi.ANSI.screenDcsStart) orelse output.len;
1089
+ try testing.expect(next_start - content_start <= Terminal.SCREEN_PASSTHROUGH_CHUNK_SIZE + ansi.ANSI.screenDcsEnd.len);
1090
+ frame_start = next_start;
1091
+ }
1092
+ }
1093
+
1094
+ test "writeClipboard - base64 encodes raw UTF-8 bytes" {
1095
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1096
+
1097
+ var env = std.process.EnvMap.init(testing.allocator);
1098
+ defer env.deinit();
1099
+ var term = Terminal.init(.{ .env_map = &env });
1100
+
1101
+ var writer = TestWriter.init(testing.allocator);
1102
+ defer writer.deinit();
1103
+ try term.writeClipboard(&writer, .clipboard, "世界 café 🚀");
1104
+
1105
+ try testing.expectEqualStrings("\x1b]52;c;5LiW55WMIGNhZsOpIPCfmoA=\x1b\\", writer.getWritten());
1106
+ }
1107
+
1108
+ test "writeClipboard - handles base64 padding and encoding chunk boundaries" {
1109
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1110
+
1111
+ var env = std.process.EnvMap.init(testing.allocator);
1112
+ defer env.deinit();
1113
+ var term = Terminal.init(.{ .env_map = &env });
1114
+ var writer = TestWriter.init(testing.allocator);
1115
+ defer writer.deinit();
1116
+
1117
+ const cases = [_]struct { raw: []const u8, encoded: []const u8 }{
1118
+ .{ .raw = "", .encoded = "" },
1119
+ .{ .raw = "a", .encoded = "YQ==" },
1120
+ .{ .raw = "ab", .encoded = "YWI=" },
1121
+ .{ .raw = "abc", .encoded = "YWJj" },
1122
+ };
1123
+ for (cases) |case| {
1124
+ writer.reset();
1125
+ try term.writeClipboard(&writer, .clipboard, case.raw);
1126
+ try testing.expectEqualStrings("\x1b]52;c;", writer.getWritten()[0..7]);
1127
+ try testing.expectEqualStrings(case.encoded, writer.getWritten()[7 .. writer.getWritten().len - 2]);
1128
+ try testing.expect(std.mem.endsWith(u8, writer.getWritten(), "\x1b\\"));
1129
+ }
1130
+
1131
+ const payload = [_]u8{'A'} ** (3 * 1024 + 1);
1132
+ const encoded_len = std.base64.standard.Encoder.calcSize(payload.len);
1133
+ const expected = try testing.allocator.alloc(u8, encoded_len);
1134
+ defer testing.allocator.free(expected);
1135
+ _ = std.base64.standard.Encoder.encode(expected, &payload);
1136
+
1137
+ writer.reset();
1138
+ try term.writeClipboard(&writer, .clipboard, &payload);
1139
+ try testing.expectEqualSlices(u8, expected, writer.getWritten()[7 .. writer.getWritten().len - 2]);
1140
+ }
1141
+
1142
+ test "clipboardSequenceSize - matches plain, tmux, and Screen output" {
1143
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1144
+
1145
+ const payload = "size me 世界";
1146
+ const environments = [_]struct { key: ?[]const u8, value: []const u8 }{
1147
+ .{ .key = null, .value = "" },
1148
+ .{ .key = "TMUX", .value = "/tmp/tmux-1000/default,12345,0" },
1149
+ .{ .key = "STY", .value = "12345.pts-0.hostname" },
1150
+ };
1151
+
1152
+ for (environments) |environment| {
1153
+ var env = std.process.EnvMap.init(testing.allocator);
1154
+ defer env.deinit();
1155
+ if (environment.key) |key| try env.put(key, environment.value);
1156
+ var term = Terminal.init(.{ .env_map = &env });
1157
+
1158
+ var writer = TestWriter.init(testing.allocator);
1159
+ defer writer.deinit();
1160
+ try term.writeClipboard(&writer, .clipboard, payload);
1161
+
1162
+ try testing.expectEqual(try term.clipboardSequenceSize(payload.len), writer.getWritten().len);
1163
+ }
1164
+ }
1165
+
1166
+ test "clipboardSequenceSize - rejects payloads beyond the FFI limit" {
1167
+ if (std.math.maxInt(usize) == std.math.maxInt(u32)) return error.SkipZigTest;
1168
+
1169
+ var term: Terminal = .{};
1170
+ try testing.expectError(
1171
+ error.ClipboardPayloadTooLarge,
1172
+ term.clipboardSequenceSize(@as(usize, Terminal.CLIPBOARD_PAYLOAD_SIZE_MAX) + 1),
1173
+ );
1174
+ }
1175
+
1176
+ test "writeClipboard - Screen framing crosses the 252-byte boundary" {
1177
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1178
+
1179
+ var env = std.process.EnvMap.init(testing.allocator);
1180
+ defer env.deinit();
1181
+ try env.put("STY", "12345.pts-0.hostname");
1182
+ var term = Terminal.init(.{ .env_map = &env });
1183
+
1184
+ const payload_one_chunk = [_]u8{'A'} ** 180;
1185
+ var writer = TestWriter.init(testing.allocator);
1186
+ defer writer.deinit();
1187
+ try term.writeClipboard(&writer, .clipboard, &payload_one_chunk);
1188
+ try testing.expectEqual(@as(usize, 1), countSubstring(writer.getWritten(), ansi.ANSI.screenDcsStart));
1189
+ try testing.expectEqual(try term.clipboardSequenceSize(payload_one_chunk.len), writer.getWritten().len);
1190
+
1191
+ writer.reset();
1192
+ const payload_two_chunks = [_]u8{'A'} ** 181;
1193
+ try term.writeClipboard(&writer, .clipboard, &payload_two_chunks);
1194
+ try testing.expectEqual(@as(usize, 2), countSubstring(writer.getWritten(), ansi.ANSI.screenDcsStart));
1195
+ try testing.expectEqual(try term.clipboardSequenceSize(payload_two_chunks.len), writer.getWritten().len);
1196
+ }
1197
+
1198
+ test "writeClipboard - wraps in DCS passthrough for tmux" {
1199
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1200
+
1201
+ var env = std.process.EnvMap.init(testing.allocator);
1202
+ defer env.deinit();
1203
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
1204
+
1205
+ var term = Terminal.init(.{ .env_map = &env });
1206
+ term.caps.osc52 = true;
1207
+
1208
+ var writer = TestWriter.init(testing.allocator);
1209
+ defer writer.deinit();
1210
+
1211
+ try term.writeClipboard(&writer, .clipboard, "test");
1212
+
1213
+ const output = writer.getWritten();
1214
+ // Should start with tmux DCS wrapper
1215
+ try testing.expect(std.mem.startsWith(u8, output, "\x1bPtmux;"));
1216
+ // Should end with DCS terminator
1217
+ try testing.expect(std.mem.endsWith(u8, output, "\x1b\\"));
1218
+ // Should have doubled ESC characters inside
1219
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b\x1b") != null);
1220
+ }
1221
+
1222
+ test "writeClipboard - wraps in DCS passthrough for GNU Screen" {
1223
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1224
+
1225
+ var env = std.process.EnvMap.init(testing.allocator);
1226
+ defer env.deinit();
1227
+ try env.put("STY", "12345.pts-0.hostname");
1228
+
1229
+ var term = Terminal.init(.{ .env_map = &env });
1230
+ term.caps.osc52 = true;
1231
+
1232
+ var writer = TestWriter.init(testing.allocator);
1233
+ defer writer.deinit();
1234
+
1235
+ try term.writeClipboard(&writer, .clipboard, "test");
1236
+
1237
+ const output = writer.getWritten();
1238
+ // Should start with DCS (but not tmux prefix)
1239
+ try testing.expect(std.mem.startsWith(u8, output, "\x1bP"));
1240
+ try testing.expect(!std.mem.startsWith(u8, output, "\x1bPtmux;"));
1241
+ // Should end with DCS terminator
1242
+ try testing.expect(std.mem.endsWith(u8, output, "\x1b\\"));
1243
+ // Should have doubled ESC characters
1244
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b\x1b") != null);
1245
+ }
1246
+
1247
+ test "writeClipboard - handles tmux sessions" {
1248
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1249
+
1250
+ var env = std.process.EnvMap.init(testing.allocator);
1251
+ defer env.deinit();
1252
+ try env.put("TMUX", "/tmp/tmux-1000/default,12345,0");
1253
+
1254
+ var term = Terminal.init(.{ .env_map = &env });
1255
+ term.caps.osc52 = true;
1256
+
1257
+ var writer = TestWriter.init(testing.allocator);
1258
+ defer writer.deinit();
1259
+
1260
+ try term.writeClipboard(&writer, .clipboard, "test");
1261
+
1262
+ const output = writer.getWritten();
1263
+ // Should have tmux DCS wrapper
1264
+ try testing.expect(std.mem.startsWith(u8, output, "\x1bPtmux;"));
1265
+ // Should end with DCS terminator
1266
+ try testing.expect(std.mem.endsWith(u8, output, "\x1b\\"));
1267
+ // Should have doubled ESC characters
1268
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b\x1b") != null);
1269
+ }
1270
+
1271
+ test "caps.osc52 - clipboard capability flag" {
1272
+ var term = Terminal.init(.{});
1273
+
1274
+ term.caps.osc52 = false;
1275
+ try testing.expect(!term.caps.osc52);
1276
+
1277
+ term.caps.osc52 = true;
1278
+ try testing.expect(term.caps.osc52);
1279
+ }
1280
+
1281
+ fn countSubstring(haystack: []const u8, needle: []const u8) usize {
1282
+ var count: usize = 0;
1283
+ var i: usize = 0;
1284
+ while (i < haystack.len) {
1285
+ if (std.mem.startsWith(u8, haystack[i..], needle)) {
1286
+ count += 1;
1287
+ i += needle.len;
1288
+ } else {
1289
+ i += 1;
1290
+ }
1291
+ }
1292
+ return count;
1293
+ }
1294
+
1295
+ test "queryTerminalSend - skips OSC 66 queries when OPENTUI_FORCE_EXPLICIT_WIDTH=false" {
1296
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1297
+
1298
+ var env = std.process.EnvMap.init(testing.allocator);
1299
+ defer env.deinit();
1300
+ try env.put("OPENTUI_FORCE_EXPLICIT_WIDTH", "false");
1301
+
1302
+ var term = Terminal.init(.{ .env_map = &env });
1303
+
1304
+ var writer = TestWriter.init(testing.allocator);
1305
+ defer writer.deinit();
1306
+
1307
+ try term.queryTerminalSend(&writer);
1308
+
1309
+ const output = writer.getWritten();
1310
+
1311
+ // Should not contain OSC 66 queries
1312
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]66;") == null);
1313
+
1314
+ // Should still contain other queries
1315
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[>0q") != null); // xtversion
1316
+
1317
+ // Verify the flag was set correctly
1318
+ try testing.expect(term.skip_explicit_width_query);
1319
+ try testing.expect(!term.caps.explicit_width);
1320
+ }
1321
+
1322
+ test "queryTerminalSend - sends OSC 66 queries by default" {
1323
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1324
+
1325
+ var env = std.process.EnvMap.init(testing.allocator);
1326
+ defer env.deinit();
1327
+
1328
+ var term = Terminal.init(.{ .env_map = &env });
1329
+
1330
+ var writer = TestWriter.init(testing.allocator);
1331
+ defer writer.deinit();
1332
+
1333
+ try term.queryTerminalSend(&writer);
1334
+
1335
+ const output = writer.getWritten();
1336
+
1337
+ // Should contain OSC 66 explicit width query
1338
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]66;w=1; \x1b\\") != null);
1339
+
1340
+ // Should contain OSC 66 scaled text query
1341
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]66;s=2; \x1b\\") != null);
1342
+
1343
+ // Verify the flag was not set
1344
+ try testing.expect(!term.skip_explicit_width_query);
1345
+ }
1346
+
1347
+ test "queryTerminalSend - sends OSC 66 queries when OPENTUI_FORCE_EXPLICIT_WIDTH=true" {
1348
+ if (builtin.os.tag == .windows) return error.SkipZigTest;
1349
+
1350
+ var env = std.process.EnvMap.init(testing.allocator);
1351
+ defer env.deinit();
1352
+ try env.put("OPENTUI_FORCE_EXPLICIT_WIDTH", "true");
1353
+
1354
+ var term = Terminal.init(.{ .env_map = &env });
1355
+
1356
+ var writer = TestWriter.init(testing.allocator);
1357
+ defer writer.deinit();
1358
+
1359
+ try term.queryTerminalSend(&writer);
1360
+
1361
+ const output = writer.getWritten();
1362
+
1363
+ // Should contain OSC 66 queries
1364
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]66;w=1; \x1b\\") != null);
1365
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]66;s=2; \x1b\\") != null);
1366
+
1367
+ // Verify the capability was forced on
1368
+ try testing.expect(term.caps.explicit_width);
1369
+ try testing.expect(!term.skip_explicit_width_query);
1370
+ }
1371
+
1372
+ test "enableDetectedFeatures - sends initial theme queries" {
1373
+ var env = std.process.EnvMap.init(testing.allocator);
1374
+ defer env.deinit();
1375
+
1376
+ var term = Terminal.init(.{ .env_map = &env });
1377
+ var writer = TestWriter.init(testing.allocator);
1378
+ defer writer.deinit();
1379
+
1380
+ try term.enableDetectedFeatures(&writer, false);
1381
+
1382
+ const output = writer.getWritten();
1383
+
1384
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.colorSchemeSet) != null);
1385
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]10;?\x07") != null);
1386
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b]11;?\x07") != null);
1387
+ try testing.expect(std.mem.indexOf(u8, output, "\x1b[?996n") == null);
1388
+ try testing.expect(term.state.theme_queries_sent);
1389
+ }
1390
+
1391
+ test "setMouseMode - enable without movement keeps click/drag only" {
1392
+ var term = Terminal.init(.{});
1393
+ var writer = TestWriter.init(testing.allocator);
1394
+ defer writer.deinit();
1395
+
1396
+ try term.setMouseMode(&writer, true, false);
1397
+
1398
+ const output = writer.getWritten();
1399
+ const idx_disable_any = std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking).?;
1400
+ const idx_enable_mouse = std.mem.indexOf(u8, output, ansi.ANSI.enableMouseTracking).?;
1401
+ const idx_enable_button = std.mem.indexOf(u8, output, ansi.ANSI.enableButtonEventTracking).?;
1402
+ const idx_enable_sgr = std.mem.indexOf(u8, output, ansi.ANSI.enableSGRMouseMode).?;
1403
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.enableAnyEventTracking) == null);
1404
+ try testing.expect(idx_disable_any < idx_enable_mouse);
1405
+ try testing.expect(idx_enable_mouse < idx_enable_button);
1406
+ try testing.expect(idx_enable_button < idx_enable_sgr);
1407
+
1408
+ try testing.expect(term.state.mouse);
1409
+ try testing.expect(!term.state.mouse_movement);
1410
+ }
1411
+
1412
+ test "setMouseMode - enable with movement enables any-event tracking" {
1413
+ var term = Terminal.init(.{});
1414
+ var writer = TestWriter.init(testing.allocator);
1415
+ defer writer.deinit();
1416
+
1417
+ try term.setMouseMode(&writer, true, true);
1418
+
1419
+ const output = writer.getWritten();
1420
+ const idx_enable_mouse = std.mem.indexOf(u8, output, ansi.ANSI.enableMouseTracking).?;
1421
+ const idx_enable_button = std.mem.indexOf(u8, output, ansi.ANSI.enableButtonEventTracking).?;
1422
+ const idx_enable_any = std.mem.indexOf(u8, output, ansi.ANSI.enableAnyEventTracking).?;
1423
+ const idx_enable_sgr = std.mem.indexOf(u8, output, ansi.ANSI.enableSGRMouseMode).?;
1424
+ try testing.expect(idx_enable_mouse < idx_enable_button);
1425
+ try testing.expect(idx_enable_button < idx_enable_any);
1426
+ try testing.expect(idx_enable_any < idx_enable_sgr);
1427
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking) == null);
1428
+
1429
+ try testing.expect(term.state.mouse);
1430
+ try testing.expect(term.state.mouse_movement);
1431
+ }
1432
+
1433
+ test "restoreTerminalModes - respects mouse movement setting" {
1434
+ var term = Terminal.init(.{});
1435
+ term.state.mouse = true;
1436
+ term.state.mouse_movement = false;
1437
+
1438
+ var writer = TestWriter.init(testing.allocator);
1439
+ defer writer.deinit();
1440
+
1441
+ try term.restoreTerminalModes(&writer);
1442
+
1443
+ const output = writer.getWritten();
1444
+ const idx_disable_any = std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking).?;
1445
+ const idx_enable_mouse = std.mem.indexOf(u8, output, ansi.ANSI.enableMouseTracking).?;
1446
+ const idx_enable_button = std.mem.indexOf(u8, output, ansi.ANSI.enableButtonEventTracking).?;
1447
+ const idx_enable_sgr = std.mem.indexOf(u8, output, ansi.ANSI.enableSGRMouseMode).?;
1448
+ try testing.expect(idx_disable_any < idx_enable_mouse);
1449
+ try testing.expect(idx_enable_mouse < idx_enable_button);
1450
+ try testing.expect(idx_enable_button < idx_enable_sgr);
1451
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.enableAnyEventTracking) == null);
1452
+ }
1453
+
1454
+ test "resetState - force-disables mouse when cleanup is pending and state drifted false" {
1455
+ var term = Terminal.init(.{});
1456
+ term.state.mouse = false;
1457
+ term.state.mouse_movement = false;
1458
+ term.state.mouse_was_enabled = true;
1459
+
1460
+ var writer = TestWriter.init(testing.allocator);
1461
+ defer writer.deinit();
1462
+
1463
+ try term.resetState(&writer);
1464
+
1465
+ const output = writer.getWritten();
1466
+ const idx_disable_any = std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking).?;
1467
+ const idx_disable_button = std.mem.indexOf(u8, output, ansi.ANSI.disableButtonEventTracking).?;
1468
+ const idx_disable_mouse = std.mem.indexOf(u8, output, ansi.ANSI.disableMouseTracking).?;
1469
+ const idx_disable_sgr = std.mem.indexOf(u8, output, ansi.ANSI.disableSGRMouseMode).?;
1470
+ try testing.expect(idx_disable_any < idx_disable_button);
1471
+ try testing.expect(idx_disable_button < idx_disable_mouse);
1472
+ try testing.expect(idx_disable_mouse < idx_disable_sgr);
1473
+ try testing.expect(!term.state.mouse);
1474
+ }
1475
+
1476
+ test "resetState - skips mouse disable when mouse was never enabled" {
1477
+ var term = Terminal.init(.{});
1478
+
1479
+ var writer = TestWriter.init(testing.allocator);
1480
+ defer writer.deinit();
1481
+
1482
+ try term.resetState(&writer);
1483
+
1484
+ const output = writer.getWritten();
1485
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking) == null);
1486
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.disableButtonEventTracking) == null);
1487
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.disableMouseTracking) == null);
1488
+ try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.disableSGRMouseMode) == null);
1489
+ }