runtime: support Bun 1.4 (#1400)

This commit is contained in:
Simon Klee
2026-09-04 21:09:49 +02:00
committed by GitHub
parent f1b320aa74
commit 0733b5382c
42 changed files with 236 additions and 205 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
BUN_VERSION: 1.3.14
BUN_VERSION: 1.4.1
ZIG_VERSION: 0.16.0
jobs:
+37 -4
View File
@@ -29,7 +29,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
@@ -102,7 +102,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Node
uses: actions/setup-node@v6
@@ -190,16 +190,49 @@ jobs:
with:
name: crash-dumps-core-${{ matrix.os }}-${{ github.run_attempt }}
bun-1-compat:
name: Bun 1.3 compatibility
needs: build
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup development Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.1
- name: Install dependencies
run: bun install
- name: Download native artifacts
uses: actions/download-artifact@v8
with:
name: native-libraries
path: packages/core/node_modules/@opentui/
- name: Setup runtime Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Test portable FFI arguments
working-directory: packages/core
run: |
test "$(bun --version)" = "1.3.14"
bun test src/tests/ffi-borrowed-pointer-callsites.test.ts
# Gate job for branch protection
build-complete:
name: Core - Build and Test
needs: [test]
needs: [test, bun-1-compat]
runs-on: blacksmith-4vcpu-ubuntu-2404
if: always()
steps:
- name: Check test results
run: |
if [ "${{ needs.test.result }}" != "success" ]; then
if [ "${{ needs.test.result }}" != "success" ] || [ "${{ needs['bun-1-compat'].result }}" != "success" ]; then
echo "Tests failed"
exit 1
fi
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Install dependencies FIRST (before copying built packages)
run: |
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
@@ -99,7 +99,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
+3 -3
View File
@@ -35,7 +35,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
@@ -98,7 +98,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
@@ -146,7 +146,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
@@ -101,7 +101,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Node
uses: actions/setup-node@v6
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
OPENTUI_INDEX_DIR: ${{ github.workspace }}/opentui-index/packages
with:
path: ./packages/web
package-manager: bun@1.3.14
package-manager: bun@1.4.1
node-version: 26
deploy:
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Install dependencies
run: bun install
@@ -49,7 +49,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Install dependencies
run: bun install
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
if: steps.run-mode.outputs.run == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Install dependencies
if: steps.run-mode.outputs.run == 'true'
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Prepare release versions
if: ${{ inputs.prepareVersion != '' }}
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Setup Zig
uses: mlugg/setup-zig@v2
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
+7 -4
View File
@@ -41,11 +41,14 @@ terminal or platform is unavailable locally.
`u32`/`u64`, not backend-only ABI names such as `usize`, `napi_env`, or `napi_value`; represent `i64`/`u64` as `bigint`,
native booleans as `0`/`1`, and shared pointers as `number | bigint`.
- Default to `buffer` for transient, non-null `TypedArray` parameters and pass the view directly. Do not call `ptr()`.
- Use `ptr` only when a parameter can be null, accepts a numeric native address, is a callback, or receives a raw
`ArrayBuffer`. Pass transient owner objects directly to `ptr` parameters; do not pre-resolve them.
- On Bun 1.3.14, `DataView` is not accepted by `buffer` or `ptr`. You MUST pass an equivalent typed array such as
- For a raw `ArrayBuffer`, create one `Uint8Array` view over it, keep that view, and declare `buffer`. A `buffer` call
costs less than a `ptr` call. `buffer` rejects an argument that is not a view with a `TypeError`. `ptr` sends a bad
argument to native code as an address.
- Use `ptr` only when a parameter can be null, accepts a numeric native address, or is a callback. Pass transient owner
objects directly to `ptr` parameters. Do not pre-resolve them.
- Bun 1.3 does not accept `DataView` directly for `buffer` or `ptr` parameters. Pass an equivalent typed array such as
`new Uint8Array(view.buffer, view.byteOffset, view.byteLength)`.
- On Bun 1.4+, a non-null `DataView` MUST use `buffer` and be passed directly.
- On Bun 1.4+, use `buffer` when you pass a `DataView` directly to FFI.
- Use `ptr(view)` only when native code stores the address beyond the call. Before resolving it, access `view.buffer` to
move any inline typed-array storage into a stable `ArrayBuffer`, then keep the view alive for the complete native
lifetime. The order is required: `const owner = view.buffer; const address = ptr(view)`. Calling `ptr(view)` first and
+1 -1
View File
@@ -49,7 +49,7 @@ Add `-g` to install the skill globally.
## Development
Development requires Bun 1.3.0 or later and Zig 0.16.0.
Development requires Bun 1.4.1 or later and Zig 0.16.0.
See [AGENTS.md](AGENTS.md) for engineering, tooling, and verification conventions.
+11 -63
View File
@@ -21,7 +21,7 @@
},
"devDependencies": {
"@opentui/keymap": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"commander": "^13.1.0",
"typescript": "^5",
@@ -57,7 +57,7 @@
"web-tree-sitter": "0.25.10",
},
"devDependencies": {
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/three": "0.177.0",
"typescript": "^5",
@@ -72,7 +72,7 @@
"devDependencies": {
"@opentui/react": "workspace:*",
"@opentui/solid": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/react": "^19.2.0",
"react": ">=19.2.0",
@@ -105,7 +105,7 @@
"devDependencies": {
"@opentui/react": "workspace:*",
"@opentui/solid": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"typescript": "^5",
},
@@ -127,7 +127,7 @@
},
"devDependencies": {
"@opentui/keymap": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/react": "^19.2.0",
"@types/react-reconciler": "^0.33.0",
@@ -162,7 +162,7 @@
"devDependencies": {
"@opentui/keymap": "workspace:*",
"@types/babel__core": "7.20.5",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"solid-js": "1.9.12",
"typescript": "^5",
@@ -190,7 +190,7 @@
"@opentui/core": "workspace:*",
"@opentui/react": "workspace:*",
"@opentui/solid": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/react": "^19.2.0",
"@types/ssh2": "^1.15.0",
@@ -211,7 +211,7 @@
"three": "0.177.0",
},
"devDependencies": {
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/three": "0.177.0",
"commander": "^13.1.0",
@@ -235,7 +235,7 @@
},
"devDependencies": {
"@opentui/qrcode": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/headless": "^5.5.0",
"typescript": "^5",
@@ -677,56 +677,6 @@
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.2", "", { "os": "android", "cpu": "arm" }, "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.2", "", { "os": "android", "cpu": "arm64" }, "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.2", "", { "os": "linux", "cpu": "arm" }, "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.2", "", { "os": "linux", "cpu": "arm" }, "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q=="],
"@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="],
@@ -759,7 +709,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/bun": ["@types/bun@1.4.1", "", { "dependencies": { "bun-types": "1.4.1" } }, "sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
@@ -863,7 +813,7 @@
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bun-types": ["bun-types@1.4.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA=="],
"bun-webgpu": ["bun-webgpu@0.1.7", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.7", "bun-webgpu-darwin-x64": "^0.1.7", "bun-webgpu-linux-x64": "^0.1.7", "bun-webgpu-win32-x64": "^0.1.7" } }, "sha512-KUxUp+oQIf7pPBMD4Hv1TUu7DWaOZ4ciKulTk9to9+Uc8yHoYrMW7L2SJCJ4FHHkywgf/7aLRgRx0b7i6DvGIQ=="],
@@ -1405,8 +1355,6 @@
"rolldown": ["rolldown@1.2.3", "", { "dependencies": { "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.3", "@rolldown/binding-darwin-arm64": "1.2.3", "@rolldown/binding-darwin-x64": "1.2.3", "@rolldown/binding-freebsd-x64": "1.2.3", "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", "@rolldown/binding-linux-arm64-gnu": "1.2.3", "@rolldown/binding-linux-arm64-musl": "1.2.3", "@rolldown/binding-linux-ppc64-gnu": "1.2.3", "@rolldown/binding-linux-s390x-gnu": "1.2.3", "@rolldown/binding-linux-x64-gnu": "1.2.3", "@rolldown/binding-linux-x64-musl": "1.2.3", "@rolldown/binding-openharmony-arm64": "1.2.3", "@rolldown/binding-win32-arm64-msvc": "1.2.3", "@rolldown/binding-win32-x64-msvc": "1.2.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A=="],
"rollup": ["rollup@4.55.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.2", "@rollup/rollup-android-arm64": "4.55.2", "@rollup/rollup-darwin-arm64": "4.55.2", "@rollup/rollup-darwin-x64": "4.55.2", "@rollup/rollup-freebsd-arm64": "4.55.2", "@rollup/rollup-freebsd-x64": "4.55.2", "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", "@rollup/rollup-linux-arm-musleabihf": "4.55.2", "@rollup/rollup-linux-arm64-gnu": "4.55.2", "@rollup/rollup-linux-arm64-musl": "4.55.2", "@rollup/rollup-linux-loong64-gnu": "4.55.2", "@rollup/rollup-linux-loong64-musl": "4.55.2", "@rollup/rollup-linux-ppc64-gnu": "4.55.2", "@rollup/rollup-linux-ppc64-musl": "4.55.2", "@rollup/rollup-linux-riscv64-gnu": "4.55.2", "@rollup/rollup-linux-riscv64-musl": "4.55.2", "@rollup/rollup-linux-s390x-gnu": "4.55.2", "@rollup/rollup-linux-x64-gnu": "4.55.2", "@rollup/rollup-linux-x64-musl": "4.55.2", "@rollup/rollup-openbsd-x64": "4.55.2", "@rollup/rollup-openharmony-arm64": "4.55.2", "@rollup/rollup-win32-arm64-msvc": "4.55.2", "@rollup/rollup-win32-ia32-msvc": "4.55.2", "@rollup/rollup-win32-x64-gnu": "4.55.2", "@rollup/rollup-win32-x64-msvc": "4.55.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg=="],
"s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+1
View File
@@ -1,6 +1,7 @@
{
"name": "@opentui",
"private": true,
"packageManager": "bun@1.4.1",
"workspaces": [
"packages/*",
"packages/solid/examples"
+2
View File
@@ -52,6 +52,8 @@ Then build your first app with the [quickstart](https://opentui.com/docs/getting
`@opentui/core` runs on Bun 1.3.0 or later, or on Node.js 26.4.0 or later with ECMAScript modules (ESM) and
`--experimental-ffi`.
Use Bun 1.4.0 or later on native Windows arm64.
The native ABI and the generated platform packages, such as `@opentui/core-linux-x64`, are internal distribution
surfaces, not application APIs.
+3 -2
View File
@@ -58,7 +58,7 @@
"license": "MIT",
"devDependencies": {
"@opentui/keymap": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"commander": "^13.1.0",
"typescript": "^5",
@@ -129,6 +129,7 @@
}
},
"engines": {
"bun": ">=1.3.0"
"bun": ">=1.3.0",
"node": ">=26.4.0"
}
}
+1
View File
@@ -585,6 +585,7 @@ if (buildLib) {
homepage: packageJson.homepage,
repository: packageJson.repository,
bugs: packageJson.bugs,
engines: packageJson.engines,
exports,
dependencies: packageJson.dependencies,
peerDependencies: packageJson.peerDependencies,
+6 -1
View File
@@ -481,7 +481,12 @@ function assertNodeStaticImportFailure(
}
function installAndTest(nodeDir: string, bunDir: string): void {
runCommand("npm", ["install", "--ignore-scripts", "--no-package-lock"], nodeDir, "Node dist test install failed")
runCommand(
"npm",
["install", "--engine-strict", "--ignore-scripts", "--no-package-lock"],
nodeDir,
"Node dist test install failed",
)
runCommand(nodePath, ["-e", `import(${JSON.stringify(packageJson.name)})`], nodeDir, "Node import smoke check failed")
runCommand(nodePath, ["--experimental-ffi", "--no-warnings", "index.mjs"], nodeDir, "Node dist smoke tests failed", {
timeout: 60_000,
+8 -5
View File
@@ -1352,8 +1352,9 @@ export class CliRenderer extends EventEmitter implements RenderContext {
private removeExitListeners(): void {
if (!this._exitListenersAdded || this.exitSignals.length === 0) return
const processEvents = process as EventEmitter
this.exitSignals.forEach((signal) => {
process.removeListener(signal, this.exitHandler)
processEvents.removeListener(signal, this.exitHandler)
})
this._exitListenersAdded = false
@@ -4388,12 +4389,14 @@ export class CliRenderer extends EventEmitter implements RenderContext {
this.lib.cancelKittyImageTransport(this.rendererPtr, false)
}
// Bun 1.4's types narrow process.removeListener to its memoryPressure overload.
const processEvents = process as EventEmitter
if (this._usesProcessStdout) {
process.removeListener("SIGWINCH", this.sigwinchHandler)
processEvents.removeListener("SIGWINCH", this.sigwinchHandler)
}
process.removeListener("uncaughtException", this.handleError)
process.removeListener("unhandledRejection", this.handleError)
process.removeListener("warning", this.warningHandler)
processEvents.removeListener("uncaughtException", this.handleError)
processEvents.removeListener("unhandledRejection", this.handleError)
processEvents.removeListener("warning", this.warningHandler)
this.removeExitListeners()
if (this.resizeTimeoutId !== null) {
@@ -68,6 +68,10 @@ function fieldOffset(struct: { layoutByName: Map<string, { offset: number }> },
return field.offset
}
function dataView(bytes: Uint8Array): DataView {
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
}
function readPackedColor(packed: ArrayBuffer, offset: number): number[] {
// 64-bit StyledChunk/CursorStyleOptions layout; matches the supported
// x64/arm64 native targets.
@@ -77,6 +81,15 @@ function readPackedColor(packed: ArrayBuffer, offset: number): number[] {
}
describe("borrowed pointer call sites", () => {
test("passes reusable struct views through the native FFI boundary", () => {
const buffer = lib.createEditBuffer("wcwidth")
try {
expect(lib.editBufferGetCursorPosition(buffer)).toEqual({ row: 0, col: 0, offset: 0 })
} finally {
lib.destroyEditBuffer(buffer)
}
})
test("preserves the native viewport lookup success result", () => {
const x = new Uint32Array([1])
const y = new Uint32Array([1])
@@ -180,7 +193,7 @@ describe("borrowed pointer call sites", () => {
textBufferViewMeasureForDimensions: symbols.textBufferViewMeasureForDimensions,
audioGetStreamStats: symbols.audioGetStreamStats,
}
const outputBuffers: Record<string, ArrayBuffer[]> = {
const outputViews: Record<string, Uint8Array[]> = {
logical: [],
visual: [],
measure: [],
@@ -191,25 +204,25 @@ describe("borrowed pointer call sites", () => {
let lineCount = 2
let bytesReceived = 20n
symbols.editBufferGetCursorPosition = (_buffer, output: ArrayBuffer) => {
outputBuffers.logical.push(output)
LogicalCursorStruct.packInto({ row: 0, col: logicalOffset, offset: logicalOffset++ }, new DataView(output), 0)
symbols.editBufferGetCursorPosition = (_buffer, output: Uint8Array) => {
outputViews.logical.push(output)
LogicalCursorStruct.packInto({ row: 0, col: logicalOffset, offset: logicalOffset++ }, dataView(output), 0)
}
symbols.editorViewGetVisualCursor = (_view, output: ArrayBuffer) => {
outputBuffers.visual.push(output)
symbols.editorViewGetVisualCursor = (_view, output: Uint8Array) => {
outputViews.visual.push(output)
VisualCursorStruct.packInto(
{ visualRow: 0, visualCol: visualOffset, logicalRow: 0, logicalCol: visualOffset, offset: visualOffset++ },
new DataView(output),
dataView(output),
0,
)
}
symbols.textBufferViewMeasureForDimensions = (_view, _width, _height, output: ArrayBuffer) => {
outputBuffers.measure.push(output)
MeasureResultStruct.packInto({ lineCount: lineCount++, widthColsMax: 8 }, new DataView(output), 0)
symbols.textBufferViewMeasureForDimensions = (_view, _width, _height, output: Uint8Array) => {
outputViews.measure.push(output)
MeasureResultStruct.packInto({ lineCount: lineCount++, widthColsMax: 8 }, dataView(output), 0)
return 1
}
symbols.audioGetStreamStats = (_engine, _streamId, output: ArrayBuffer) => {
outputBuffers.audio.push(output)
symbols.audioGetStreamStats = (_engine, _streamId, output: Uint8Array) => {
outputViews.audio.push(output)
AudioStreamStatsStruct.packInto(
{
bytesReceived: bytesReceived++,
@@ -224,7 +237,7 @@ describe("borrowed pointer call sites", () => {
errorCode: 0,
readyGeneration: 1,
},
new DataView(output),
dataView(output),
0,
)
return 0
@@ -255,17 +268,17 @@ describe("borrowed pointer call sites", () => {
expect(audioFirst.bytesReceived).toBe(20n)
expect(audioSecond.bytesReceived).toBe(21n)
for (const buffers of Object.values(outputBuffers)) {
expect(buffers).toHaveLength(2)
expect(buffers[0]).toBeInstanceOf(ArrayBuffer)
expect(buffers[1]).toBe(buffers[0])
for (const views of Object.values(outputViews)) {
expect(views).toHaveLength(2)
expect(views[0]).toBeInstanceOf(Uint8Array)
expect(views[1]).toBe(views[0])
}
} finally {
Object.assign(symbols, originals)
}
})
test("grid draw repacks and forwards one owning ArrayBuffer", () => {
test("grid draw repacks and forwards one owning Uint8Array", () => {
const calls: any[][] = []
const original = symbols.bufferDrawGrid
const fg = RGBA.fromValues(1, 1, 1, 1)
@@ -278,16 +291,16 @@ describe("borrowed pointer call sites", () => {
drawInner: true,
drawOuter: false,
})
const firstBuffer = calls[0]![8] as ArrayBuffer
expect(firstBuffer).toBeInstanceOf(ArrayBuffer)
expect(new Uint8Array(firstBuffer).slice(0, GridDrawOptionsStruct.size)).toEqual(new Uint8Array([1, 0]))
const firstView = calls[0]![8] as Uint8Array
expect(firstView).toBeInstanceOf(Uint8Array)
expect(firstView).toEqual(new Uint8Array([1, 0]))
lib.bufferDrawGrid(1 as any, chars, fg, bg, offsets, 1, offsets, 1, {
drawInner: false,
drawOuter: true,
})
expect(calls[1]![8]).toBe(firstBuffer)
expect(new Uint8Array(firstBuffer).slice(0, GridDrawOptionsStruct.size)).toEqual(new Uint8Array([0, 1]))
expect(calls[1]![8]).toBe(firstView)
expect(firstView).toEqual(new Uint8Array([0, 1]))
} finally {
symbols.bufferDrawGrid = original
}
@@ -313,41 +326,43 @@ describe("borrowed pointer call sites", () => {
try {
for (const [index, name] of logicalQueries.entries()) {
originals.set(name, symbols[name])
const outputBuffers: ArrayBuffer[] = []
const outputViews: Uint8Array[] = []
let offset = index + 2
symbols[name] = (...args: any[]) => {
const output = args.at(-1) as ArrayBuffer
outputBuffers.push(output)
LogicalCursorStruct.packInto({ row: index, col: index + 1, offset: offset++ }, new DataView(output), 0)
const output = args.at(-1) as Uint8Array
outputViews.push(output)
LogicalCursorStruct.packInto({ row: index, col: index + 1, offset: offset++ }, dataView(output), 0)
}
const first = (lib as any)[name](1)
const second = (lib as any)[name](1)
expect(first).not.toBe(second)
expect(first).toEqual({ row: index, col: index + 1, offset: index + 2 })
expect(second).toEqual({ row: index, col: index + 1, offset: index + 3 })
expect(outputBuffers[1]).toBe(outputBuffers[0])
expect(outputViews[0]).toBeInstanceOf(Uint8Array)
expect(outputViews[1]).toBe(outputViews[0])
}
originals.set("editBufferOffsetToPosition", symbols.editBufferOffsetToPosition)
const positionBuffers: ArrayBuffer[] = []
symbols.editBufferOffsetToPosition = (_buffer, offset: number, output: ArrayBuffer) => {
positionBuffers.push(output)
LogicalCursorStruct.packInto({ row: 1, col: 2, offset }, new DataView(output), 0)
const positionViews: Uint8Array[] = []
symbols.editBufferOffsetToPosition = (_buffer, offset: number, output: Uint8Array) => {
positionViews.push(output)
LogicalCursorStruct.packInto({ row: 1, col: 2, offset }, dataView(output), 0)
return 1
}
const firstPosition = lib.editBufferOffsetToPosition(1 as any, 9)
const secondPosition = lib.editBufferOffsetToPosition(1 as any, 10)
expect(firstPosition).toEqual({ row: 1, col: 2, offset: 9 })
expect(secondPosition).toEqual({ row: 1, col: 2, offset: 10 })
expect(positionBuffers[1]).toBe(positionBuffers[0])
expect(positionViews[0]).toBeInstanceOf(Uint8Array)
expect(positionViews[1]).toBe(positionViews[0])
for (const [index, name] of visualQueries.entries()) {
originals.set(name, symbols[name])
const outputBuffers: ArrayBuffer[] = []
const outputViews: Uint8Array[] = []
let offset = index + 4
symbols[name] = (...args: any[]) => {
const output = args.at(-1) as ArrayBuffer
outputBuffers.push(output)
const output = args.at(-1) as Uint8Array
outputViews.push(output)
VisualCursorStruct.packInto(
{
visualRow: index,
@@ -356,7 +371,7 @@ describe("borrowed pointer call sites", () => {
logicalCol: index + 3,
offset: offset++,
},
new DataView(output),
dataView(output),
0,
)
}
@@ -365,7 +380,8 @@ describe("borrowed pointer call sites", () => {
expect(first).not.toBe(second)
expect(first.offset).toBe(index + 4)
expect(second.offset).toBe(index + 5)
expect(outputBuffers[1]).toBe(outputBuffers[0])
expect(outputViews[0]).toBeInstanceOf(Uint8Array)
expect(outputViews[1]).toBe(outputViews[0])
}
} finally {
for (const [name, original] of originals) symbols[name] = original
@@ -602,7 +618,7 @@ describe("borrowed pointer call sites", () => {
}
})
test("audioCloseStream forwards its reason and unpacks the owned output buffer", () => {
test("audioCloseStream forwards its reason and unpacks the owned output view", () => {
const calls: any[][] = []
const original = symbols.audioCloseStream
symbols.audioCloseStream = (...args: any[]) => {
@@ -621,7 +637,7 @@ describe("borrowed pointer call sites", () => {
errorCode: -3,
readyGeneration: 7,
},
new DataView(args[3]),
dataView(args[3] as Uint8Array),
0,
)
return 0
@@ -632,7 +648,7 @@ describe("borrowed pointer call sites", () => {
expect(calls[0]![0]).toBe(11)
expect(calls[0]![1]).toBe(22)
expect(calls[0]![2]).toBe(NativeAudioStreamCloseReason.TransportError)
expect(calls[0]![3]).toBeInstanceOf(ArrayBuffer)
expect(calls[0]![3]).toBeInstanceOf(Uint8Array)
expect(result).toEqual({
status: 0,
stats: {
@@ -817,9 +833,9 @@ describe("borrowed pointer call sites", () => {
lib.imageComposite(handle, handle, 0, 0, 0, 255)
expect(calls.get("bufferDrawImage")![2]).toBe(firstImageOptions)
expect(firstImageOptions).toBeInstanceOf(ArrayBuffer)
expect((firstImageOptions as ArrayBuffer).byteLength).toBe(ImageDrawOptionsStruct.size)
expect(ImageDrawOptionsStruct.unpack(firstImageOptions as ArrayBuffer)).toMatchObject({
expect(firstImageOptions).toBeInstanceOf(Uint8Array)
expect((firstImageOptions as Uint8Array).byteLength).toBe(ImageDrawOptionsStruct.size)
expect(ImageDrawOptionsStruct.unpack((firstImageOptions as Uint8Array).buffer)).toMatchObject({
x: 2,
y: 3,
width: 4,
+53 -44
View File
@@ -102,6 +102,15 @@ import { isBunfsPath } from "./lib/bunfs.js"
import { resolveNativeLibraryPath } from "#opentui/runtime-assets"
import { allocStruct } from "bun-ffi-structs"
// Struct outputs use `buffer` instead of `ptr`. A `buffer` call is about 3x cheaper on Bun 1.3 and 2.5x cheaper on
// Bun 1.4. `buffer` rejects an argument that is not a view with a TypeError. `ptr` accepts a number as a raw address.
// `buffer` also skips the Node pointer normalizer. Bun 1.3 rejects ArrayBuffer and DataView for `buffer`, so keep one
// Uint8Array view per reusable struct.
function allocFFIStruct(structDefinition: Parameters<typeof allocStruct>[0]) {
const storage = allocStruct(structDefinition)
return { ...storage, ffiView: new Uint8Array(storage.buffer) }
}
registerEnvVar({
name: "OPENTUI_LIBC",
description: "Select Linux native libc package. Supported values: glibc, musl.",
@@ -454,11 +463,11 @@ function getOpenTUILib(libPath?: string) {
returns: "i32",
},
embeddedTerminalCursor: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "i32",
},
embeddedTerminalEncodeKey: {
args: ["u32", "ptr", "ptr", "u32", "ptr", "u32", "ptr", "u32", "ptr"],
args: ["u32", "buffer", "ptr", "u32", "ptr", "u32", "ptr", "u32", "ptr"],
returns: "i32",
},
embeddedTerminalEncodeMouse: {
@@ -818,7 +827,7 @@ function getOpenTUILib(libPath?: string) {
returns: "void",
},
bufferDrawImage: {
args: ["u32", "u32", "ptr"],
args: ["u32", "u32", "buffer"],
returns: "u8",
},
bufferDrawPackedBuffer: {
@@ -834,7 +843,7 @@ function getOpenTUILib(libPath?: string) {
returns: "void",
},
bufferDrawGrid: {
args: ["u32", "buffer", "buffer", "buffer", "buffer", "u32", "buffer", "u32", "ptr"],
args: ["u32", "buffer", "buffer", "buffer", "buffer", "u32", "buffer", "u32", "buffer"],
returns: "void",
},
bufferDrawBox: {
@@ -1207,7 +1216,7 @@ function getOpenTUILib(libPath?: string) {
returns: "void",
},
textBufferViewMeasureForDimensions: {
args: ["u32", "u32", "u32", "ptr"],
args: ["u32", "u32", "u32", "buffer"],
returns: "bool",
},
bufferDrawTextBufferView: {
@@ -1359,7 +1368,7 @@ function getOpenTUILib(libPath?: string) {
returns: "void",
},
editBufferGetCursorPosition: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editBufferGetId: {
@@ -1403,19 +1412,19 @@ function getOpenTUILib(libPath?: string) {
returns: "void",
},
editBufferGetNextWordBoundary: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editBufferGetPrevWordBoundary: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editBufferGetEOL: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editBufferOffsetToPosition: {
args: ["u32", "u32", "ptr"],
args: ["u32", "u32", "buffer"],
returns: "bool",
},
editBufferPositionToOffset: {
@@ -1495,7 +1504,7 @@ function getOpenTUILib(libPath?: string) {
// EditorView VisualCursor methods
editorViewGetVisualCursor: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
@@ -1516,23 +1525,23 @@ function getOpenTUILib(libPath?: string) {
returns: "void",
},
editorViewGetNextWordBoundary: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editorViewGetPrevWordBoundary: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editorViewGetEOL: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editorViewGetVisualSOL: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editorViewGetVisualEOL: {
args: ["u32", "ptr"],
args: ["u32", "buffer"],
returns: "void",
},
editorViewGotoVisualLineEnd: {
@@ -1958,11 +1967,11 @@ function getOpenTUILib(libPath?: string) {
returns: "i32",
},
audioGetStreamStats: {
args: ["u32", "u32", "ptr"],
args: ["u32", "u32", "buffer"],
returns: "i32",
},
audioCloseStream: {
args: ["u32", "u32", "u32", "ptr"],
args: ["u32", "u32", "u32", "buffer"],
returns: "i32",
},
audioLoad: {
@@ -3225,11 +3234,11 @@ class FFIRenderLib implements RenderLib {
private readonly yogaLayout = new Float32Array(6)
private readonly ffiStructStorage = {
logicalCursor: {
...allocStruct(LogicalCursorStruct),
...allocFFIStruct(LogicalCursorStruct),
result: { row: 0, col: 0, offset: 0 } as LogicalCursor,
},
visualCursor: {
...allocStruct(VisualCursorStruct),
...allocFFIStruct(VisualCursorStruct),
result: {
visualRow: 0,
visualCol: 0,
@@ -3239,13 +3248,13 @@ class FFIRenderLib implements RenderLib {
} as VisualCursor,
},
measureResult: {
...allocStruct(MeasureResultStruct),
...allocFFIStruct(MeasureResultStruct),
result: { lineCount: 0, widthColsMax: 0 } as MeasureResult,
},
embeddedTerminalCursor: allocStruct(EmbeddedTerminalCursorStruct),
embeddedTerminalKeyOptions: allocStruct(EmbeddedTerminalKeyOptionsStruct),
embeddedTerminalCursor: allocFFIStruct(EmbeddedTerminalCursorStruct),
embeddedTerminalKeyOptions: allocFFIStruct(EmbeddedTerminalKeyOptionsStruct),
audioStreamStats: {
...allocStruct(AudioStreamStatsStruct),
...allocFFIStruct(AudioStreamStatsStruct),
result: {
bytesReceived: 0n,
framesDecoded: 0n,
@@ -3260,8 +3269,8 @@ class FFIRenderLib implements RenderLib {
readyGeneration: 0,
} as NativeAudioStreamStats,
},
imageDrawOptions: allocStruct(ImageDrawOptionsStruct),
gridDrawOptions: allocStruct(GridDrawOptionsStruct),
imageDrawOptions: allocFFIStruct(ImageDrawOptionsStruct),
gridDrawOptions: allocFFIStruct(GridDrawOptionsStruct),
}
private disposed = false
private clipboardServices = new Set<ClipboardServiceHandle>()
@@ -3400,7 +3409,7 @@ class FFIRenderLib implements RenderLib {
public embeddedTerminalCursor(handle: EmbeddedTerminalHandle): EmbeddedTerminalCursor {
const storage = this.ffiStructStorage.embeddedTerminalCursor
embeddedTerminalResult(this.opentui.symbols.embeddedTerminalCursor(handle, storage.buffer), "cursor query")
embeddedTerminalResult(this.opentui.symbols.embeddedTerminalCursor(handle, storage.ffiView), "cursor query")
const result = EmbeddedTerminalCursorStruct.unpack(storage.buffer)
return {
x: result.x,
@@ -3436,7 +3445,7 @@ class FFIRenderLib implements RenderLib {
const encode = (output: Uint8Array) =>
this.opentui.symbols.embeddedTerminalEncodeKey(
handle,
options.buffer,
options.ffiView,
keyCodeLength === 0 ? null : keyCode,
keyCodeLength,
textLength === 0 ? null : text,
@@ -4063,7 +4072,7 @@ class FFIRenderLib implements RenderLib {
storage.view,
0,
)
return Boolean(this.opentui.symbols.bufferDrawImage(buffer, image, storage.buffer))
return Boolean(this.opentui.symbols.bufferDrawImage(buffer, image, storage.ffiView))
}
public bufferDrawPackedBuffer(
@@ -4156,7 +4165,7 @@ class FFIRenderLib implements RenderLib {
columnCount,
rowOffsets,
rowCount,
this.ffiStructStorage.gridDrawOptions.buffer,
this.ffiStructStorage.gridDrawOptions.ffiView,
)
}
@@ -5373,7 +5382,7 @@ class FFIRenderLib implements RenderLib {
public textBufferViewMeasureForDimensions(view: Pointer, width: number, height: number): MeasureResult | null {
const storage = this.ffiStructStorage.measureResult
const success = this.opentui.symbols.textBufferViewMeasureForDimensions(view, width, height, storage.buffer)
const success = this.opentui.symbols.textBufferViewMeasureForDimensions(view, width, height, storage.ffiView)
if (!success) return null
const result = MeasureResultStruct.unpackInto(storage.view, storage.result)
return { lineCount: result.lineCount, widthColsMax: result.widthColsMax }
@@ -5686,7 +5695,7 @@ class FFIRenderLib implements RenderLib {
public editBufferGetCursorPosition(buffer: Pointer): LogicalCursor {
const storage = this.ffiStructStorage.logicalCursor
this.opentui.symbols.editBufferGetCursorPosition(buffer, storage.buffer)
this.opentui.symbols.editBufferGetCursorPosition(buffer, storage.ffiView)
const cursor = LogicalCursorStruct.unpackInto(storage.view, storage.result)
return { row: cursor.row, col: cursor.col, offset: cursor.offset }
}
@@ -5745,28 +5754,28 @@ class FFIRenderLib implements RenderLib {
public editBufferGetNextWordBoundary(buffer: Pointer): LogicalCursor {
const storage = this.ffiStructStorage.logicalCursor
this.opentui.symbols.editBufferGetNextWordBoundary(buffer, storage.buffer)
this.opentui.symbols.editBufferGetNextWordBoundary(buffer, storage.ffiView)
const cursor = LogicalCursorStruct.unpackInto(storage.view, storage.result)
return { row: cursor.row, col: cursor.col, offset: cursor.offset }
}
public editBufferGetPrevWordBoundary(buffer: Pointer): LogicalCursor {
const storage = this.ffiStructStorage.logicalCursor
this.opentui.symbols.editBufferGetPrevWordBoundary(buffer, storage.buffer)
this.opentui.symbols.editBufferGetPrevWordBoundary(buffer, storage.ffiView)
const cursor = LogicalCursorStruct.unpackInto(storage.view, storage.result)
return { row: cursor.row, col: cursor.col, offset: cursor.offset }
}
public editBufferGetEOL(buffer: Pointer): LogicalCursor {
const storage = this.ffiStructStorage.logicalCursor
this.opentui.symbols.editBufferGetEOL(buffer, storage.buffer)
this.opentui.symbols.editBufferGetEOL(buffer, storage.ffiView)
const cursor = LogicalCursorStruct.unpackInto(storage.view, storage.result)
return { row: cursor.row, col: cursor.col, offset: cursor.offset }
}
public editBufferOffsetToPosition(buffer: Pointer, offset: number): LogicalCursor | null {
const storage = this.ffiStructStorage.logicalCursor
const success = this.opentui.symbols.editBufferOffsetToPosition(buffer, offset, storage.buffer)
const success = this.opentui.symbols.editBufferOffsetToPosition(buffer, offset, storage.ffiView)
if (!success) return null
const cursor = LogicalCursorStruct.unpackInto(storage.view, storage.result)
return { row: cursor.row, col: cursor.col, offset: cursor.offset }
@@ -5966,7 +5975,7 @@ class FFIRenderLib implements RenderLib {
public editorViewGetVisualCursor(view: Pointer): VisualCursor {
const storage = this.ffiStructStorage.visualCursor
this.opentui.symbols.editorViewGetVisualCursor(view, storage.buffer)
this.opentui.symbols.editorViewGetVisualCursor(view, storage.ffiView)
const cursor = VisualCursorStruct.unpackInto(storage.view, storage.result)
return { ...cursor }
}
@@ -5989,35 +5998,35 @@ class FFIRenderLib implements RenderLib {
public editorViewGetNextWordBoundary(view: Pointer): VisualCursor {
const storage = this.ffiStructStorage.visualCursor
this.opentui.symbols.editorViewGetNextWordBoundary(view, storage.buffer)
this.opentui.symbols.editorViewGetNextWordBoundary(view, storage.ffiView)
const cursor = VisualCursorStruct.unpackInto(storage.view, storage.result)
return { ...cursor }
}
public editorViewGetPrevWordBoundary(view: Pointer): VisualCursor {
const storage = this.ffiStructStorage.visualCursor
this.opentui.symbols.editorViewGetPrevWordBoundary(view, storage.buffer)
this.opentui.symbols.editorViewGetPrevWordBoundary(view, storage.ffiView)
const cursor = VisualCursorStruct.unpackInto(storage.view, storage.result)
return { ...cursor }
}
public editorViewGetEOL(view: Pointer): VisualCursor {
const storage = this.ffiStructStorage.visualCursor
this.opentui.symbols.editorViewGetEOL(view, storage.buffer)
this.opentui.symbols.editorViewGetEOL(view, storage.ffiView)
const cursor = VisualCursorStruct.unpackInto(storage.view, storage.result)
return { ...cursor }
}
public editorViewGetVisualSOL(view: Pointer): VisualCursor {
const storage = this.ffiStructStorage.visualCursor
this.opentui.symbols.editorViewGetVisualSOL(view, storage.buffer)
this.opentui.symbols.editorViewGetVisualSOL(view, storage.ffiView)
const cursor = VisualCursorStruct.unpackInto(storage.view, storage.result)
return { ...cursor }
}
public editorViewGetVisualEOL(view: Pointer): VisualCursor {
const storage = this.ffiStructStorage.visualCursor
this.opentui.symbols.editorViewGetVisualEOL(view, storage.buffer)
this.opentui.symbols.editorViewGetVisualEOL(view, storage.ffiView)
const cursor = VisualCursorStruct.unpackInto(storage.view, storage.result)
return { ...cursor }
}
@@ -6371,7 +6380,7 @@ class FFIRenderLib implements RenderLib {
public audioGetStreamStats(engine: AudioEngineHandle, streamId: number): NativeAudioStreamStats | null {
const storage = this.ffiStructStorage.audioStreamStats
const status = this.opentui.symbols.audioGetStreamStats(engine, streamId, storage.buffer)
const status = this.opentui.symbols.audioGetStreamStats(engine, streamId, storage.ffiView)
if (status !== 0) return null
const stats = AudioStreamStatsStruct.unpackInto(storage.view, storage.result) as NativeAudioStreamStats
return { ...stats }
@@ -6383,7 +6392,7 @@ class FFIRenderLib implements RenderLib {
reason: NativeAudioStreamCloseReason,
): { status: number; stats: NativeAudioStreamStats | null } {
const storage = this.ffiStructStorage.audioStreamStats
const status = this.opentui.symbols.audioCloseStream(engine, streamId, reason, storage.buffer)
const status = this.opentui.symbols.audioCloseStream(engine, streamId, reason, storage.ffiView)
if (status !== 0) return { status, stats: null }
const stats = AudioStreamStatsStruct.unpackInto(storage.view, storage.result) as NativeAudioStreamStats
return { status, stats: { ...stats } }
+1 -1
View File
@@ -31,7 +31,7 @@
"web-tree-sitter": "0.25.10"
},
"devDependencies": {
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/three": "0.177.0",
"typescript": "^5"
+1 -1
View File
@@ -90,7 +90,7 @@
"devDependencies": {
"@opentui/react": "workspace:*",
"@opentui/solid": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/react": "^19.2.0",
"react": ">=19.2.0",
+1 -1
View File
@@ -49,7 +49,7 @@
"devDependencies": {
"@opentui/react": "workspace:*",
"@opentui/solid": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"typescript": "^5"
},
+1 -1
View File
@@ -47,7 +47,7 @@
},
"devDependencies": {
"@opentui/keymap": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/react": "^19.2.0",
"@types/react-reconciler": "^0.33.0",
+1 -1
View File
@@ -56,7 +56,7 @@
"devDependencies": {
"@opentui/keymap": "workspace:*",
"@types/babel__core": "7.20.5",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"solid-js": "1.9.12",
"typescript": "^5"
+4 -2
View File
@@ -40,8 +40,10 @@ npm install @opentui/ssh
`@opentui/core` is a peer dependency. Supported runtimes are Bun ≥ 1.3.0 and
Node.js ≥ 26.4.0. CI runs the SSH integration suite with Bun on macOS, Linux,
and Windows, and installs, imports, starts, and closes the packed ESM package
on Node.js.
and Windows. CI also installs, imports, starts, and closes the packed ESM
package on Node.js.
Use Bun ≥ 1.4.0 on native Windows arm64.
## The shape: `createServer(config).serve(handler)`
+1 -1
View File
@@ -35,7 +35,7 @@
"@opentui/core": "workspace:*",
"@opentui/react": "workspace:*",
"@opentui/solid": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/react": "^19.2.0",
"@types/ssh2": "^1.15.0",
+1 -1
View File
@@ -37,7 +37,7 @@
"planck": "^1.4.2"
},
"devDependencies": {
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@types/node": "^24.0.0",
"@types/three": "0.177.0",
"commander": "^13.1.0",
+1
View File
@@ -134,6 +134,7 @@ writeFileSync(
homepage: packageJson.homepage,
repository: packageJson.repository,
bugs: packageJson.bugs,
engines: packageJson.engines,
exports,
dependencies: processedDependencies,
optionalDependencies: packageJson.optionalDependencies,
+1 -1
View File
@@ -31,7 +31,7 @@
},
"devDependencies": {
"@opentui/qrcode": "workspace:*",
"@types/bun": "1.3.14",
"@types/bun": "1.4.1",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/headless": "^5.5.0",
"typescript": "^5"
@@ -77,7 +77,7 @@ A failed automatic start destroys the partial native engine. Successful automati
Audio uses the same native runtime as the renderer. Read [Runtime and platform support](/docs/getting-started/runtime-support) for setup and distribution details.
The local requirements are Bun 1.3.0 or later, or Node.js 26.4.0 or later with experimental FFI. Native packages support macOS, Linux, and Windows on x64 and arm64.
The local requirements are Bun 1.3.0 or later, or Node.js 26.4.0 or later with experimental FFI. Native packages support macOS, Linux, and Windows on x64 and arm64. Use Bun 1.4.0 or later on native Windows arm64.
Linux selects glibc by default. Set the documented libc selector when the target needs musl. Audio has no browser target.
@@ -19,6 +19,8 @@ Check the runtime, native artifact, permission, asset, and package requirements
The Node.js acceptance scripts accept Node.js 26.4 or later.
Use Bun 1.4.0 or later on native Windows arm64 because Bun 1.3 does not support FFI on that target.
Use ESM for Node.js applications. The Core root has an asynchronous ESM graph. A CommonJS `require("@opentui/core")`
call fails with `ERR_REQUIRE_ASYNC_MODULE`.
@@ -380,7 +380,8 @@ The package root works in Bun and Node.js. Each framework path re-exports the fu
## `@opentui/ssh`
The package supports Bun 1.3.0 and Node.js 26.4.0 or later. It has one public entry point.
The package supports Bun 1.3.0 or later and Node.js 26.4.0 or later. Use Bun 1.4.0 or later on native Windows arm64.
It has one public entry point.
| Symbols | Class | Canonical documentation |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------- |
@@ -21,7 +21,8 @@ SSH is an access transport. It is not an application packaging format. See
bun add @opentui/ssh @opentui/core
```
`@opentui/core` is a peer dependency. The SSH package declares Bun >=1.3.0 and Node.js >=26.4.0. The SSH Node CI lane
`@opentui/core` is a peer dependency. The SSH package declares Bun >=1.3.0 and Node.js >=26.4.0. Use Bun >=1.4.0 on
native Windows arm64. The SSH Node CI lane
runs on Linux x64. Its packed Node consumer test uses a Core stub, so that test does not start the native renderer. See
[Runtime and platform support](/docs/getting-started/runtime-support) before you run the native renderer with Node.js.
@@ -12,10 +12,12 @@ skill:
## Requirements
`@opentui/three` requires Bun >=1.3.0. It does not support Node.js. The package pins Three.js 0.177.0. Its manifest lists
`bun-webgpu` 0.1.7, `@dimforge/rapier2d-simd-compat`, and `planck` as optional dependencies, but the root statically
imports all three through its renderer and physics exports. They must all resolve even when an application does not
use physics. A normal install includes them. An install with optional dependencies omitted makes the root import fail.
`@opentui/three` requires Bun >=1.3.0. It does not support Node.js.
The package pins Three.js 0.177.0. Its manifest lists `bun-webgpu` 0.1.7, `@dimforge/rapier2d-simd-compat`, and `planck`
as optional dependencies. The package root imports all three through its renderer and physics exports. All three
dependencies must resolve even when an application does not use physics. A normal install includes them. The root
module does not load when the installation omits optional dependencies.
```bash
bun add @opentui/three
@@ -24,7 +24,7 @@ categories:
`@opentui/ssh` maps each accepted SSH shell to a `CliRenderer`. The renderer dimensions track the client's terminal.
The package requires Bun `>=1.3.0` or Node.js `>=26.4.0`.
The package requires Bun `>=1.3.0` or Node.js `>=26.4.0`. Use Bun `>=1.4.0` on native Windows arm64.
Open authentication accepts SSH `none` without credentials. Use it only for loopback development. Do not expose open
authentication to a public or untrusted network.