From 552c7717fc0546ea323470d35a187cd7942127c3 Mon Sep 17 00:00:00 2001 From: "Andrew C. Oliver" Date: Fri, 5 Dec 2025 16:36:35 -0300 Subject: [PATCH 1/7] Add preliminary terminal image support --- bun.lock | 80 ++++++- packages/core/package.json | 1 + packages/core/src/graphics/protocol.ts | 41 ++++ packages/core/src/renderables/Image.ts | 54 +++++ packages/core/src/renderables/index.ts | 1 + packages/core/src/renderer.ts | 204 +++++++++++++++++- packages/core/src/testing/test-recorder.ts | 6 +- .../core/src/tests/graphics.protocol.test.ts | 58 +++++ .../core/src/tests/renderer.control.test.ts | 4 +- .../core/src/tests/renderer.images.test.ts | 71 ++++++ packages/core/src/types.ts | 3 + packages/react/jsx-namespace.d.ts | 2 + packages/react/package.json | 2 +- packages/react/src/components/index.ts | 2 + packages/react/src/types/components.ts | 4 + 15 files changed, 520 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/graphics/protocol.ts create mode 100644 packages/core/src/renderables/Image.ts create mode 100644 packages/core/src/tests/graphics.protocol.test.ts create mode 100644 packages/core/src/tests/renderer.images.test.ts diff --git a/bun.lock b/bun.lock index ef4c4a197..3ec4160f9 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "@opentui", @@ -15,6 +14,7 @@ "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", + "sharp": "0.33.5", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -161,6 +161,46 @@ "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], @@ -227,6 +267,18 @@ "@opentui/core": ["@opentui/core@workspace:packages/core"], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.57", "", { "os": "darwin", "cpu": "arm64" }, "sha512-by+Pvh5aKw13zSuNbwQKAthrlCpdI7eU8HuIEN/PPQdHuQivtgxkMn9jgLEwIMkOnrvZ8SqdFb392zZRgSCNDg=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.57", "", { "os": "darwin", "cpu": "x64" }, "sha512-Upcem+gU4I2/znG7IoR3l7VklZTmhxD2E4QV98sY/FAz4blD+ng/n4M3fCj+M03ZnSkoulELkPzWXH0AwIkcyw=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.57", "", { "os": "linux", "cpu": "arm64" }, "sha512-0YAqzuKNLEm+NBQVSni1/pd8960Ybf8LMw5Ead5m4BNtPzYQ5QkdUEAIhsdViQFnFlCPo9AI12oRBzurGMgSmw=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.57", "", { "os": "linux", "cpu": "x64" }, "sha512-2JTGolyVbuKY+DcE7V15DQwjOLygCHFgFzYwrsLCGxoaEgnEAcocWxAF0CdSlvx01/eY4u8uspzqOT94XTUJRQ=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.57", "", { "os": "win32", "cpu": "arm64" }, "sha512-HPT9fPzOPaLZ2P35Ed6N1icWDgLHpzxnqKsBfDG3bZq5ClhfHGuXm00JmekxauIhR6zTTYd2WSfMCB2qpCufrw=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.57", "", { "os": "win32", "cpu": "x64" }, "sha512-Q7PcfqQjlz3idOBns3KbKMrTm15Zx4SMtReNX8BTbVP1UsLDAtIif+1JqjPx1YFRq5WCCsuRAggJaUEmQp5Nqw=="], + "@opentui/react": ["@opentui/react@workspace:packages/react"], "@opentui/solid": ["@opentui/solid@workspace:packages/solid"], @@ -321,6 +373,14 @@ "caniuse-lite": ["caniuse-lite@1.0.30001737", "", {}, "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -329,6 +389,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], "electron-to-chromium": ["electron-to-chromium@1.5.211", "", {}, "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw=="], @@ -371,6 +433,8 @@ "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], @@ -465,12 +529,16 @@ "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ=="], + "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="], "solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="], @@ -491,6 +559,8 @@ "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -517,6 +587,12 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@opentui/react/@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@opentui/react/@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], diff --git a/packages/core/package.json b/packages/core/package.json index 7fe37abd9..5c0a75532 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,6 +37,7 @@ "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", + "sharp": "0.33.5", "yoga-layout": "3.2.1" }, "peerDependencies": { diff --git a/packages/core/src/graphics/protocol.ts b/packages/core/src/graphics/protocol.ts new file mode 100644 index 000000000..46d19d93a --- /dev/null +++ b/packages/core/src/graphics/protocol.ts @@ -0,0 +1,41 @@ +import { env, registerEnvVar } from "../lib/env" +import type { RenderContext } from "../types" + +export type ImageProtocol = "kitty" | "iterm2" | "none" + +export interface GraphicsSupport { + readonly protocol: ImageProtocol +} + +registerEnvVar({ + name: "OTUI_PREFER_KITTY_GRAPHICS", + description: "Force-enable kitty graphics protocol when available.", + type: "boolean", + default: false, +}) + +export function detectGraphicsSupport(): GraphicsSupport { + const termProgram = process.env["TERM_PROGRAM"] ?? "" + const term = process.env["TERM"] ?? "" + if (termProgram === "iTerm.app") { + return { protocol: "iterm2" } + } + if (term.toLowerCase().includes("kitty") || env.OTUI_PREFER_KITTY_GRAPHICS) { + return { protocol: "kitty" } + } + return { protocol: "none" } +} + +export function encodeItermImage(image: Buffer, widthPx: number, heightPx: number): string { + const base64 = image.toString("base64") + return `\u001b]1337;File=inline=1;width=${widthPx}px;height=${heightPx}px;preserveAspectRatio=1:${base64}\u0007` +} + +export function encodeKittyImage(id: number, image: Buffer, widthPx: number, heightPx: number): string { + const base64 = image.toString("base64") + return `\u001b_Gf=100,a=T,s=${widthPx},v=${heightPx},i=${id};${base64}\u001b\\` +} + +export function encodeKittyDelete(id: number): string { + return `\u001b_Ga=d,d=0,i=${id}\u001b\\` +} diff --git a/packages/core/src/renderables/Image.ts b/packages/core/src/renderables/Image.ts new file mode 100644 index 000000000..bf3184edf --- /dev/null +++ b/packages/core/src/renderables/Image.ts @@ -0,0 +1,54 @@ +import { Renderable, type RenderableOptions } from "../Renderable" +import type { RenderContext } from "../types" +import type { OptimizedBuffer } from "../buffer" +import { RGBA, parseColor } from "../lib/RGBA" +import type { GraphicsSupport } from "../graphics/protocol" + +export type ImageFit = "contain" | "cover" | "fill" + +export interface ImageOptions extends RenderableOptions { + src?: string | Buffer + alt?: string + width?: number + height?: number + fit?: ImageFit + pixelWidth?: number + pixelHeight?: number +} + +export class ImageRenderable extends Renderable { + src?: string | Buffer + alt?: string + fit: ImageFit + pixelWidth?: number + pixelHeight?: number + constructor(ctx: RenderContext, options: ImageOptions) { + super(ctx, options) + this.src = options.src + this.alt = options.alt + this.fit = options.fit ?? "contain" + this.width = options.width ?? 0 + this.height = options.height ?? 0 + this.pixelWidth = options.pixelWidth + this.pixelHeight = options.pixelHeight + } + + protected renderSelf(buffer: OptimizedBuffer, _deltaTime: number): void { + const width = Math.max(this.width, 0) + const height = Math.max(this.height, 0) + if (width === 0 || height === 0) return + + // Clear the target area so previous frame contents do not bleed through + buffer.fillRect(this.x, this.y, width, height, RGBA.fromInts(0, 0, 0, 0)) + + const graphics = (this._ctx.graphicsSupport ?? null) as GraphicsSupport | null + const shouldShowFallback = !graphics || graphics.protocol === "none" || !this.src + if (!shouldShowFallback) return + + const fallback = this.alt ?? "" + if (fallback.length === 0) return + + const trimmed = fallback.slice(0, Math.max(width, 1)) + buffer.drawText(trimmed, this.x, this.y, parseColor("#A0A0A0")) + } +} diff --git a/packages/core/src/renderables/index.ts b/packages/core/src/renderables/index.ts index 2abdd9827..607c60847 100644 --- a/packages/core/src/renderables/index.ts +++ b/packages/core/src/renderables/index.ts @@ -6,6 +6,7 @@ export * from "./composition/VRenderable" export * from "./composition/vnode" export * from "./Diff" export * from "./FrameBuffer" +export * from "./Image" export * from "./Input" export * from "./LineNumberRenderable" export * from "./ScrollBar" diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 606fd9d8e..54f6939db 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -14,6 +14,14 @@ import { resolveRenderLib, type RenderLib } from "./zig" import { TerminalConsole, type ConsoleOptions, capture } from "./console" import { MouseParser, type MouseEventType, type RawMouseEvent, type ScrollInfo } from "./lib/parse.mouse" import { Selection } from "./lib/selection" +import { clamp } from "./lib/utils" +import { + detectGraphicsSupport, + encodeItermImage, + encodeKittyDelete, + encodeKittyImage, + type GraphicsSupport, +} from "./graphics/protocol" import { EventEmitter } from "events" import { destroySingleton, hasSingleton, singleton } from "./lib/singleton" import { getObjectsInViewport } from "./lib/objects-in-viewport" @@ -32,6 +40,7 @@ import { isPixelResolutionResponse, parsePixelResolution, } from "./lib/terminal-capability-detection" +import { ImageRenderable, type ImageFit } from "./renderables/Image" registerEnvVar({ name: "OTUI_DUMP_CAPTURES", @@ -98,6 +107,10 @@ export type PixelResolution = { width: number height: number } +export type CellMetrics = { + pxPerCellX: number + pxPerCellY: number +} export class MouseEvent { public readonly type: MouseEventType @@ -348,6 +361,25 @@ export class CliRenderer extends EventEmitter implements RenderContext { private idleResolvers: (() => void)[] = [] + private _graphicsSupport: GraphicsSupport = detectGraphicsSupport() + private kittyImageId = 1 + private imageCache: Map< + number, + { + srcKey: string + x: number + y: number + width: number + height: number + fit: ImageFit + pixelWidth?: number + pixelHeight?: number + data: Buffer + kittyId?: number + } + > = new Map() + private cellMetrics: CellMetrics | null = null + private _debugInputs: Array<{ timestamp: string; sequence: string }> = [] private _debugModeEnabled: boolean = env.OTUI_DEBUG @@ -397,6 +429,10 @@ export class CliRenderer extends EventEmitter implements RenderContext { return this._controlState } + public get graphicsSupport(): GraphicsSupport { + return this._graphicsSupport + } + constructor( lib: RenderLib, rendererPtr: Pointer, @@ -503,7 +539,7 @@ export class CliRenderer extends EventEmitter implements RenderContext { // Prevents output from being written to the terminal, useful for debugging if (env.OTUI_NO_NATIVE_RENDER) { - this.renderNative = () => { + this.renderNative = async () => { if (this._splitHeight > 0) { this.flushStdoutCache(this._splitHeight) } @@ -896,7 +932,9 @@ export class CliRenderer extends EventEmitter implements RenderContext { if (isCapabilityResponse(sequence)) { this.lib.processCapabilityResponse(this.rendererPtr, sequence) this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr) + this.cellMetrics = null this.emit("capabilities", this._capabilities) + this.logDebug(`capabilities updated: ${JSON.stringify(this._capabilities)}`) return true } return false @@ -920,14 +958,30 @@ export class CliRenderer extends EventEmitter implements RenderContext { } this.addInputHandler((sequence: string) => { - if (isPixelResolutionResponse(sequence) && this.waitingForPixelResolution) { + if (isPixelResolutionResponse(sequence)) { const resolution = parsePixelResolution(sequence) if (resolution) { this._resolution = resolution + this._capabilities = { ...(this._capabilities ?? {}), pixelResolution: resolution } + this.cellMetrics = null this.waitingForPixelResolution = false + this.requestRender() + this.emit("pixelResolution", resolution) + this.logDebug(`pixelResolution response: ${JSON.stringify(resolution)}`) + return true } - return true + this.logDebug(`pixelResolution parse failed for sequence: ${JSON.stringify(sequence)}`) + return false + } + + if (env.OTUI_DEBUG) { + const numeric = sequence + .split("") + .map((c) => c.charCodeAt(0)) + .join(",") + this.logDebug(`unhandled sequence: text=${JSON.stringify(sequence)} codes=[${numeric}]`) } + return false }) this.addInputHandler(this.capabilityHandler) @@ -1183,6 +1237,7 @@ export class CliRenderer extends EventEmitter implements RenderContext { this._terminalWidth = width this._terminalHeight = height + this.cellMetrics = null this.queryPixelResolution() this.capturedRenderable = undefined @@ -1591,7 +1646,7 @@ export class CliRenderer extends EventEmitter implements RenderContext { this._console.renderToBuffer(this.nextRenderBuffer) if (!this._isDestroyed) { - this.renderNative() + await this.renderNative() const overallFrameTime = performance.now() - overallStart @@ -1625,7 +1680,7 @@ export class CliRenderer extends EventEmitter implements RenderContext { this.loop() } - private renderNative(): void { + private async renderNative(): Promise { if (this.renderingNative) { console.error("Rendering called concurrently") throw new Error("Rendering called concurrently") @@ -1640,10 +1695,147 @@ export class CliRenderer extends EventEmitter implements RenderContext { this.renderingNative = true this.lib.render(this.rendererPtr, force) + await this.flushImages() // this.dumpStdoutBuffer(Date.now()) this.renderingNative = false } + public getCellMetrics(): CellMetrics | null { + if (this.cellMetrics) return this.cellMetrics + const cols = this.width + const rows = this.height + const pixelRes = (this._capabilities?.pixelResolution as PixelResolution | undefined) ?? this._resolution + if (!pixelRes || cols <= 0 || rows <= 0) return null + const pxPerCellX = pixelRes.width / cols + const pxPerCellY = pixelRes.height / rows + this.cellMetrics = { pxPerCellX, pxPerCellY } + return this.cellMetrics + } + + private async flushImages(): Promise { + if (this._graphicsSupport.protocol === "none") { + return + } + const images = this.collectImageRenderables(this.root) + const metrics = this.getCellMetrics() + const seen: Set = new Set() + for (const { renderable, x, y } of images) { + if (!renderable.src || !renderable.visible) continue + if ((renderable.pixelWidth !== undefined || renderable.pixelHeight !== undefined) && !metrics) { + continue + } + const width = Math.max(renderable.width, 1) + const height = Math.max(renderable.height, 1) + const srcKey = typeof renderable.src === "string" ? renderable.src : renderable.src.toString("base64") + const previous = this.imageCache.get(renderable.num) + let data = previous?.data + const pixelWidth = + renderable.pixelWidth ?? + (metrics ? Math.max(1, Math.round(width * metrics.pxPerCellX)) : Math.max(1, width)) + const pixelHeight = + renderable.pixelHeight ?? + (metrics ? Math.max(1, Math.round(height * metrics.pxPerCellY)) : Math.max(1, height)) + const changedImage = + !data || + previous.srcKey !== srcKey || + previous.width !== width || + previous.height !== height || + previous.fit !== renderable.fit || + previous.pixelWidth !== pixelWidth || + previous.pixelHeight !== pixelHeight + if (changedImage) { + data = await this.loadImage(renderable.src, pixelWidth, pixelHeight, renderable.fit) + } + if (!data) continue + let kittyId = previous?.kittyId + if (this._graphicsSupport.protocol === "kitty") { + if (kittyId === undefined) { + kittyId = this.kittyImageId++ + } + } + const positionChanged = !previous || previous.x !== x || previous.y !== y + const needsSend = changedImage || positionChanged + if (needsSend && previous && this._graphicsSupport.protocol === "kitty" && previous.kittyId !== undefined) { + this.writeOut(encodeKittyDelete(previous.kittyId)) + } + + this.imageCache.set(renderable.num, { + srcKey, + x, + y, + width, + height, + fit: renderable.fit, + pixelWidth, + pixelHeight, + data, + kittyId, + }) + seen.add(renderable.num) + if (!needsSend) { + continue + } + + let offsetX = 0 + let offsetY = 0 + if (metrics) { + const layoutPxWidth = width * metrics.pxPerCellX + const layoutPxHeight = height * metrics.pxPerCellY + offsetX = Math.max(0, Math.round((layoutPxWidth - pixelWidth) / (2 * metrics.pxPerCellX))) + offsetY = Math.max(0, Math.round((layoutPxHeight - pixelHeight) / (2 * metrics.pxPerCellY))) + } + + const move = `\u001b[${y + offsetY + 1};${x + offsetX + 1}H` + if (this._graphicsSupport.protocol === "iterm2") { + this.writeOut(move + encodeItermImage(data, pixelWidth, pixelHeight)) + } else if (this._graphicsSupport.protocol === "kitty") { + this.writeOut(move + encodeKittyImage(kittyId ?? this.kittyImageId++, data, pixelWidth, pixelHeight)) + } + } + + // Drop cache entries for images no longer in the tree + for (const key of this.imageCache.keys()) { + if (!seen.has(key)) { + const cached = this.imageCache.get(key) + if (cached?.kittyId !== undefined && this._graphicsSupport.protocol === "kitty") { + this.writeOut(encodeKittyDelete(cached.kittyId)) + } + this.imageCache.delete(key) + } + } + } + + private collectImageRenderables(root: Renderable): { renderable: ImageRenderable; x: number; y: number }[] { + const out: { renderable: ImageRenderable; x: number; y: number }[] = [] + const queue: Renderable[] = [root] + while (queue.length > 0) { + const current = queue.shift()! + for (const child of current.getChildren()) { + if (child instanceof ImageRenderable) { + out.push({ renderable: child, x: child.x, y: child.y }) + } + queue.push(child) + } + } + return out + } + + private async loadImage(src: string | Buffer, width: number, height: number, fit: ImageFit): Promise { + try { + const sharpModule: unknown = await import("sharp") + const moduleCandidate = sharpModule as { default?: unknown } + const sharp = + typeof moduleCandidate.default === "function" + ? (moduleCandidate.default as typeof import("sharp")) + : (sharpModule as typeof import("sharp")) + const input = typeof src === "string" ? src : Buffer.from(src) + return await sharp(input).resize({ width, height, fit }).png().toBuffer() + } catch (error) { + console.error("Failed to load image", error) + return null + } + } + private collectStatSample(frameTime: number): void { this.frameTimes.push(frameTime) if (this.frameTimes.length > this.maxStatSamples) { @@ -1779,6 +1971,8 @@ export class CliRenderer extends EventEmitter implements RenderContext { } } + private logDebug(_message: string): void {} + private notifySelectablesOfSelectionChange(): void { const selectedRenderables: Renderable[] = [] const touchedRenderables: Renderable[] = [] diff --git a/packages/core/src/testing/test-recorder.ts b/packages/core/src/testing/test-recorder.ts index 01913d59a..35c902e36 100644 --- a/packages/core/src/testing/test-recorder.ts +++ b/packages/core/src/testing/test-recorder.ts @@ -16,7 +16,7 @@ export class TestRecorder { private recording: boolean = false private frameNumber: number = 0 private startTime: number = 0 - private originalRenderNative?: () => void + private originalRenderNative?: () => Promise private decoder = new TextDecoder() constructor(renderer: TestRenderer) { @@ -40,9 +40,9 @@ export class TestRecorder { this.originalRenderNative = this.renderer["renderNative"].bind(this.renderer) // Override renderNative to capture frames after each render - this.renderer["renderNative"] = () => { + this.renderer["renderNative"] = async () => { // Call the original renderNative - this.originalRenderNative!() + await this.originalRenderNative!() // Capture the frame after rendering this.captureFrame() diff --git a/packages/core/src/tests/graphics.protocol.test.ts b/packages/core/src/tests/graphics.protocol.test.ts new file mode 100644 index 000000000..2a60859c1 --- /dev/null +++ b/packages/core/src/tests/graphics.protocol.test.ts @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { detectGraphicsSupport } from "../graphics/protocol" +import { clearEnvCache } from "../lib/env" + +const originalTermProgram = process.env.TERM_PROGRAM +const originalTerm = process.env.TERM +const originalPreferKitty = process.env.OTUI_PREFER_KITTY_GRAPHICS + +function setEnv(key: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[key] + return + } + process.env[key] = value +} + +afterEach(() => { + setEnv("TERM_PROGRAM", originalTermProgram) + setEnv("TERM", originalTerm) + setEnv("OTUI_PREFER_KITTY_GRAPHICS", originalPreferKitty) + clearEnvCache() +}) + +describe("detectGraphicsSupport", () => { + test("detects iTerm2 via TERM_PROGRAM", () => { + setEnv("TERM_PROGRAM", "iTerm.app") + setEnv("TERM", "xterm-256color") + clearEnvCache() + + expect(detectGraphicsSupport().protocol).toBe("iterm2") + }) + + test("detects kitty via TERM", () => { + setEnv("TERM_PROGRAM", "") + setEnv("TERM", "xterm-kitty") + clearEnvCache() + + expect(detectGraphicsSupport().protocol).toBe("kitty") + }) + + test("prefers kitty when override is set", () => { + setEnv("TERM_PROGRAM", "") + setEnv("TERM", "xterm-256color") + setEnv("OTUI_PREFER_KITTY_GRAPHICS", "true") + clearEnvCache() + + expect(detectGraphicsSupport().protocol).toBe("kitty") + }) + + test("falls back to none when no graphics are detected", () => { + setEnv("TERM_PROGRAM", "") + setEnv("TERM", "xterm-256color") + setEnv("OTUI_PREFER_KITTY_GRAPHICS", "false") + clearEnvCache() + + expect(detectGraphicsSupport().protocol).toBe("none") + }) +}) diff --git a/packages/core/src/tests/renderer.control.test.ts b/packages/core/src/tests/renderer.control.test.ts index af074f3a7..daefb5a06 100644 --- a/packages/core/src/tests/renderer.control.test.ts +++ b/packages/core/src/tests/renderer.control.test.ts @@ -133,7 +133,7 @@ test("requestRender() does not trigger when renderer is suspended", async () => // @ts-expect-error - renderNative is private const originalRender = renderer.renderNative.bind(renderer) // @ts-expect-error - renderNative is private - renderer.renderNative = () => { + renderer.renderNative = async () => { renderCalled = true return originalRender() } @@ -156,7 +156,7 @@ test("requestRender() does trigger when renderer is paused", async () => { // @ts-expect-error - renderNative is private const originalRender = renderer.renderNative.bind(renderer) // @ts-expect-error - renderNative is private - renderer.renderNative = () => { + renderer.renderNative = async () => { renderCalled = true return originalRender() } diff --git a/packages/core/src/tests/renderer.images.test.ts b/packages/core/src/tests/renderer.images.test.ts new file mode 100644 index 000000000..f116447df --- /dev/null +++ b/packages/core/src/tests/renderer.images.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { ImageRenderable } from "../renderables/Image" +import { createTestRenderer, type TestRenderer } from "../testing/test-renderer" +import type { GraphicsSupport } from "../graphics/protocol" + +describe("renderer image support", () => { + let renderer: TestRenderer | null = null + let renderOnce: (() => Promise) | null = null + let captureCharFrame: (() => string) | null = null + + afterEach(() => { + if (renderer) { + renderer.destroy() + renderer = null + } + }) + + test("flushes kitty image sequences", async () => { + const setup = await createTestRenderer({ width: 20, height: 10 }) + renderer = setup.renderer + renderOnce = setup.renderOnce + captureCharFrame = setup.captureCharFrame + + const writes: string[] = [] + const testHarness = renderer as unknown as { + writeOut: (chunk: string) => boolean + loadImage: (src: Buffer, width: number, height: number, fit: string) => Promise + _graphicsSupport: GraphicsSupport + } + + testHarness.writeOut = (chunk: string) => { + writes.push(chunk) + return true + } + testHarness.loadImage = async () => Buffer.from("img") + testHarness._graphicsSupport = { protocol: "kitty" } + + const image = new ImageRenderable(renderer, { + src: Buffer.from("img"), + width: 2, + height: 3, + left: 1, + top: 1, + }) + renderer.root.add(image) + + await renderOnce!() + + const expected = + `\u001b[2;2H` + `\u001b_Gf=100,a=T,s=2,v=3,i=1;${Buffer.from("img").toString("base64")}\u001b\\` + expect(writes).toContain(expected) + }) + + test("renders alt text when graphics are disabled", async () => { + const setup = await createTestRenderer({ width: 20, height: 6 }) + renderer = setup.renderer + renderOnce = setup.renderOnce + captureCharFrame = setup.captureCharFrame + + const testHarness = renderer as unknown as { _graphicsSupport: GraphicsSupport } + testHarness._graphicsSupport = { protocol: "none" } + + const image = new ImageRenderable(renderer, { alt: "Logo", width: 10, height: 2, left: 0, top: 0 }) + renderer.root.add(image) + + await renderOnce!() + + const frame = captureCharFrame!() + expect(frame).toContain("Logo") + }) +}) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7387701c3..f1146d9a9 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -3,6 +3,7 @@ import type { EventEmitter } from "events" import type { Selection } from "./lib/selection" import type { Renderable } from "./Renderable" import type { InternalKeyHandler, KeyHandler } from "./lib/KeyHandler" +import type { GraphicsSupport } from "./graphics/protocol" export const TextAttributes = { NONE: 0, @@ -65,6 +66,8 @@ export interface RenderContext extends EventEmitter { clearSelection: () => void startSelection: (renderable: Renderable, x: number, y: number) => void updateSelection: (currentRenderable: Renderable | undefined, x: number, y: number) => void + graphicsSupport?: GraphicsSupport + getCellMetrics?: () => { pxPerCellX: number; pxPerCellY: number } | null } export type Timeout = ReturnType | undefined diff --git a/packages/react/jsx-namespace.d.ts b/packages/react/jsx-namespace.d.ts index 81a85275d..85ca14d65 100644 --- a/packages/react/jsx-namespace.d.ts +++ b/packages/react/jsx-namespace.d.ts @@ -6,6 +6,7 @@ import type { DiffProps, ExtendedIntrinsicElements, InputProps, + ImageProps, LineBreakProps, LineNumberProps, OpenTUIComponents, @@ -42,6 +43,7 @@ export namespace JSX { diff: DiffProps input: InputProps textarea: TextareaProps + image: ImageProps select: SelectProps scrollbox: ScrollBoxProps "ascii-font": AsciiFontProps diff --git a/packages/react/package.json b/packages/react/package.json index 562fd1158..cc91532d5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -47,7 +47,7 @@ "react": ">=19.0.0" }, "dependencies": { - "@opentui/core": "workspace:*", + "@opentui/core": "file:../core", "react-reconciler": "^0.32.0" } } diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 26f47190d..50f99113d 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -4,6 +4,7 @@ import { CodeRenderable, DiffRenderable, InputRenderable, + ImageRenderable, LineNumberRenderable, ScrollBoxRenderable, SelectRenderable, @@ -32,6 +33,7 @@ export const baseComponents = { "ascii-font": ASCIIFontRenderable, "tab-select": TabSelectRenderable, "line-number": LineNumberRenderable, + image: ImageRenderable, // Text modifiers span: SpanRenderable, diff --git a/packages/react/src/types/components.ts b/packages/react/src/types/components.ts index f5165a0c7..c2c8a19ee 100644 --- a/packages/react/src/types/components.ts +++ b/packages/react/src/types/components.ts @@ -28,6 +28,8 @@ import type { TextNodeRenderable, TextOptions, TextRenderable, + ImageOptions, + ImageRenderable, } from "@opentui/core" import type React from "react" @@ -152,6 +154,8 @@ export type ScrollBoxProps = ComponentProps, Sc export type AsciiFontProps = ComponentProps +export type ImageProps = ComponentProps + export type TabSelectProps = ComponentProps & { focused?: boolean onChange?: (index: number, option: TabSelectOption | null) => void From 52061c3a5aaf6e0ac8d919f0c96a14e16ad038de Mon Sep 17 00:00:00 2001 From: "Andrew C. Oliver" Date: Fri, 5 Dec 2025 16:40:00 -0300 Subject: [PATCH 2/7] Fix react workspace dependency --- packages/react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index cc91532d5..562fd1158 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -47,7 +47,7 @@ "react": ">=19.0.0" }, "dependencies": { - "@opentui/core": "file:../core", + "@opentui/core": "workspace:*", "react-reconciler": "^0.32.0" } } From c8ebb3c226001b2bb548cc306522ea6181fae31b Mon Sep 17 00:00:00 2001 From: "Andrew C. Oliver" Date: Fri, 5 Dec 2025 18:58:01 -0300 Subject: [PATCH 3/7] Fix nested scrollbox clipping by intersecting scissor rects --- packages/core/src/renderables/ScrollBox.ts | 4 +- packages/core/src/tests/scrollbox.test.ts | 134 +++++++++++++++++++++ packages/core/src/zig/buffer.zig | 14 ++- packages/react/tests/layout.test.tsx | 77 +++++++++++- 4 files changed, 223 insertions(+), 6 deletions(-) diff --git a/packages/core/src/renderables/ScrollBox.ts b/packages/core/src/renderables/ScrollBox.ts index abc7a7dcf..165d93dbf 100644 --- a/packages/core/src/renderables/ScrollBox.ts +++ b/packages/core/src/renderables/ScrollBox.ts @@ -33,8 +33,8 @@ class ContentRenderable extends BoxRenderable { protected _getVisibleChildren(): number[] { if (this._viewportCulling) { - return getObjectsInViewport(this.viewport, this.getChildrenSortedByPrimaryAxis(), this.primaryAxis).map( - (child) => child.num, + return getObjectsInViewport(this.viewport, this.getChildrenSortedByPrimaryAxis(), this.primaryAxis, 0).map((child) => + child.num, ) } return this.getChildrenSortedByPrimaryAxis().map((child) => child.num) diff --git a/packages/core/src/tests/scrollbox.test.ts b/packages/core/src/tests/scrollbox.test.ts index 695eda56a..8a4a65419 100644 --- a/packages/core/src/tests/scrollbox.test.ts +++ b/packages/core/src/tests/scrollbox.test.ts @@ -79,6 +79,49 @@ describe("ScrollBoxRenderable - child delegation", () => { }) }) +describe("ScrollBoxRenderable - clipping", () => { + test("clips nested scrollbox content to inner viewport (see issue #388)", async () => { + const root = new BoxRenderable(testRenderer, { + flexDirection: "column", + gap: 0, + width: 32, + height: 16, + }) + + const outer = new ScrollBoxRenderable(testRenderer, { + width: 30, + height: 10, + border: true, + overflow: "hidden", + scrollY: true, + }) + + const inner = new ScrollBoxRenderable(testRenderer, { + width: 26, + height: 6, + border: true, + overflow: "hidden", + scrollY: true, + }) + + for (let index = 0; index < 6; index += 1) { + inner.add(new TextRenderable(testRenderer, { content: `LEAK-${index}` })) + } + + outer.add(inner) + root.add(outer) + testRenderer.root.add(root) + + await renderOnce() + + const frame = captureCharFrame() + const innerViewportHeight = 4 // height 6 minus top/bottom border + const visibleLines = frame.split("\n").filter((line) => line.includes("LEAK-")) + + expect(visibleLines.length).toBeLessThanOrEqual(innerViewportHeight) + }) +}) + describe("ScrollBoxRenderable - destroyRecursively", () => { test("destroys internal ScrollBox components", () => { const parent = new ScrollBoxRenderable(testRenderer, { id: "scroll-parent" }) @@ -1013,4 +1056,95 @@ console.log(processor.reduce((acc, val) => acc + val, 0))` expect(scrollPositions[i]).toBe(maxScrollPositions[i]) } }) + + test("clips nested scrollboxes when multiple stacked children overflow (app-style tool blocks)", async () => { + const custom = await createTestRenderer({ width: 120, height: 40 }) + const { renderer, renderOnce, captureCharFrame } = custom + + const root = new BoxRenderable(renderer, { flexDirection: "column", width: 118, height: 38, gap: 0 }) + const header = new BoxRenderable(renderer, { height: 3, border: true }) + header.add(new TextRenderable(testRenderer, { content: "HEADER" })) + root.add(header) + + const outer = new ScrollBoxRenderable(renderer, { height: 25, border: true, overflow: "hidden", scrollY: true }) + expect((outer as any)._overflow).toBe("hidden") + + const addToolBlock = (id: number) => { + const wrapper = new BoxRenderable(renderer, { border: true, padding: 0, marginTop: 0, marginBottom: 0 }) + const inner = new ScrollBoxRenderable(renderer, { + height: 10, + border: true, + overflow: "hidden", + scrollY: true, + contentOptions: { paddingTop: 0, paddingBottom: 0, paddingLeft: 0, paddingRight: 0 }, + }) + expect((inner as any)._overflow).toBe("hidden") + for (let i = 0; i < 15; i += 1) { + inner.add(new TextRenderable(renderer, { content: `[tool ${id}] line ${i}` })) + } + wrapper.add(inner) + outer.add(wrapper) + } + + addToolBlock(1) + addToolBlock(2) + addToolBlock(3) + + root.add(outer) + + const footer = new BoxRenderable(renderer, { height: 3, border: true }) + footer.add(new TextRenderable(renderer, { content: "FOOTER" })) + root.add(footer) + + renderer.root.add(root) + await renderOnce() + expect(outer.width).toBeGreaterThan(0) + expect(outer.height).toBeGreaterThan(0) + + const frame = captureCharFrame() + + // The third tool block should be clipped entirely (outer height fits ~two blocks). + expect(frame).not.toMatch(/\[tool 3\] line 1/) + + renderer.destroy() + }) + + test("does not overdraw above header when scrolling nested tool blocks upward", async () => { + const custom = await createTestRenderer({ width: 120, height: 24 }) + const { renderer, renderOnce, captureCharFrame } = custom + + const root = new BoxRenderable(renderer, { flexDirection: "column", width: 118, height: 22, gap: 0 }) + const header = new BoxRenderable(renderer, { height: 3, border: true }) + header.add(new TextRenderable(renderer, { content: "HEADER" })) + root.add(header) + + const outer = new ScrollBoxRenderable(renderer, { height: 14, border: true, overflow: "hidden", scrollY: true }) + const inner = new ScrollBoxRenderable(renderer, { height: 10, border: true, overflow: "hidden", scrollY: true }) + for (let i = 0; i < 12; i += 1) { + inner.add(new TextRenderable(renderer, { content: `[tool] line ${i}` })) + } + outer.add(inner) + root.add(outer) + + const footer = new BoxRenderable(renderer, { height: 3, border: true }) + footer.add(new TextRenderable(renderer, { content: "FOOTER" })) + root.add(footer) + + renderer.root.add(root) + await renderOnce() + + // Scroll up to try to draw above header + inner.scrollTo({ x: 0, y: -100 }) + outer.scrollTo({ x: 0, y: -100 }) + await renderOnce() + + const frame = captureCharFrame() + const headerIndex = frame.indexOf("HEADER") + const firstToolIndex = frame.indexOf("[tool] line 0") + + expect(headerIndex).toBeGreaterThan(-1) + expect(firstToolIndex).toBeGreaterThan(headerIndex) + + renderer.destroy() + }) }) diff --git a/packages/core/src/zig/buffer.zig b/packages/core/src/zig/buffer.zig index 0f637720b..594f96f54 100644 --- a/packages/core/src/zig/buffer.zig +++ b/packages/core/src/zig/buffer.zig @@ -268,12 +268,24 @@ pub const OptimizedBuffer = struct { } pub fn pushScissorRect(self: *OptimizedBuffer, x: i32, y: i32, width: u32, height: u32) !void { - const rect = ClipRect{ + var rect = ClipRect{ .x = x, .y = y, .width = width, .height = height, }; + + // Intersect with current scissor (if any) so nested scissor rects always clip to parents. + if (self.getCurrentScissorRect() != null) { + const intersect = self.clipRectToScissor(rect.x, rect.y, rect.width, rect.height); + if (intersect) |clipped| { + rect = clipped; + } else { + // Completely outside current scissor; push a degenerate rect so nothing renders. + rect = ClipRect{ .x = 0, .y = 0, .width = 0, .height = 0 }; + } + } + try self.scissor_stack.append(rect); } diff --git a/packages/react/tests/layout.test.tsx b/packages/react/tests/layout.test.tsx index 3218c4b7e..4e019699e 100644 --- a/packages/react/tests/layout.test.tsx +++ b/packages/react/tests/layout.test.tsx @@ -343,7 +343,7 @@ describe("React Renderer | Layout Tests", () => { expect(frame).toMatchSnapshot() }) - it("should render scrollbox with sticky scroll and spacer", async () => { + it("should render scrollbox with sticky scroll and spacer", async () => { testSetup = await testRender( { await testSetup.renderOnce() const frame = testSetup.captureCharFrame() - expect(frame).toMatchSnapshot() - }) + expect(frame).toMatchSnapshot() + }) + + it("should clip nested scrollbox content (React) [issue #388]", async () => { + const innerLines = Array.from({ length: 12 }, (_, i) => `LEAK-${i}`) + + testSetup = await testRender( + + HEADER + + + {innerLines.map((line) => ( + {line} + ))} + + + FOOTER + , + { + width: 52, + height: 20, + }, + ) + + await testSetup.renderOnce() + + const outer = testSetup.renderer.root.findDescendantById?.("outer-scroll") as any + const inner = testSetup.renderer.root.findDescendantById?.("inner-scroll") as any + // Force both scrollboxes to scroll to exercise translation + clipping + if (inner && typeof inner.scrollTo === "function") { + inner.scrollTo({ x: 0, y: 100 }) + } + if (outer && typeof outer.scrollTo === "function") { + outer.scrollTo({ x: 0, y: 50 }) + } + await testSetup.renderOnce() + + const frame = testSetup.captureCharFrame() + const visibleLeakLines = frame.split("\n").filter((line) => line.includes("LEAK-")) + + // The inner viewport height is 4 (6 minus 2 for borders). Currently, the renderer leaks and shows more. + expect(visibleLeakLines.length).toBeLessThanOrEqual(4) + + // Ensure header/footer are still present for context + expect(frame).toContain("HEADER") + expect(frame).toContain("FOOTER") + }) }) describe("Empty and Edge Cases", () => { From 2dbbcca6cdf4fed1e316a0fbac91f7a30f489cdb Mon Sep 17 00:00:00 2001 From: "Andrew C. Oliver" Date: Mon, 8 Dec 2025 19:29:36 -0300 Subject: [PATCH 4/7] Fix renderer image formatting and types --- packages/core/src/renderer.ts | 23 ++++++++----------- .../core/src/tests/renderer.images.test.ts | 3 +-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 54f6939db..3b33cd963 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -14,7 +14,6 @@ import { resolveRenderLib, type RenderLib } from "./zig" import { TerminalConsole, type ConsoleOptions, capture } from "./console" import { MouseParser, type MouseEventType, type RawMouseEvent, type ScrollInfo } from "./lib/parse.mouse" import { Selection } from "./lib/selection" -import { clamp } from "./lib/utils" import { detectGraphicsSupport, encodeItermImage, @@ -1728,21 +1727,19 @@ export class CliRenderer extends EventEmitter implements RenderContext { const height = Math.max(renderable.height, 1) const srcKey = typeof renderable.src === "string" ? renderable.src : renderable.src.toString("base64") const previous = this.imageCache.get(renderable.num) - let data = previous?.data + let data: Buffer | null = previous?.data ?? null const pixelWidth = - renderable.pixelWidth ?? - (metrics ? Math.max(1, Math.round(width * metrics.pxPerCellX)) : Math.max(1, width)) + renderable.pixelWidth ?? (metrics ? Math.max(1, Math.round(width * metrics.pxPerCellX)) : Math.max(1, width)) const pixelHeight = - renderable.pixelHeight ?? - (metrics ? Math.max(1, Math.round(height * metrics.pxPerCellY)) : Math.max(1, height)) + renderable.pixelHeight ?? (metrics ? Math.max(1, Math.round(height * metrics.pxPerCellY)) : Math.max(1, height)) const changedImage = !data || - previous.srcKey !== srcKey || - previous.width !== width || - previous.height !== height || - previous.fit !== renderable.fit || - previous.pixelWidth !== pixelWidth || - previous.pixelHeight !== pixelHeight + previous?.srcKey !== srcKey || + previous?.width !== width || + previous?.height !== height || + previous?.fit !== renderable.fit || + previous?.pixelWidth !== pixelWidth || + previous?.pixelHeight !== pixelHeight if (changedImage) { data = await this.loadImage(renderable.src, pixelWidth, pixelHeight, renderable.fit) } @@ -1755,7 +1752,7 @@ export class CliRenderer extends EventEmitter implements RenderContext { } const positionChanged = !previous || previous.x !== x || previous.y !== y const needsSend = changedImage || positionChanged - if (needsSend && previous && this._graphicsSupport.protocol === "kitty" && previous.kittyId !== undefined) { + if (needsSend && previous?.kittyId !== undefined && this._graphicsSupport.protocol === "kitty") { this.writeOut(encodeKittyDelete(previous.kittyId)) } diff --git a/packages/core/src/tests/renderer.images.test.ts b/packages/core/src/tests/renderer.images.test.ts index f116447df..b0b9a517b 100644 --- a/packages/core/src/tests/renderer.images.test.ts +++ b/packages/core/src/tests/renderer.images.test.ts @@ -46,8 +46,7 @@ describe("renderer image support", () => { await renderOnce!() - const expected = - `\u001b[2;2H` + `\u001b_Gf=100,a=T,s=2,v=3,i=1;${Buffer.from("img").toString("base64")}\u001b\\` + const expected = `\u001b[2;2H` + `\u001b_Gf=100,a=T,s=2,v=3,i=1;${Buffer.from("img").toString("base64")}\u001b\\` expect(writes).toContain(expected) }) From f40c04b0dad61f6765e0a20172314e2ebdafdc21 Mon Sep 17 00:00:00 2001 From: "Andrew C. Oliver" Date: Mon, 8 Dec 2025 20:04:19 -0300 Subject: [PATCH 5/7] Pin Bun to 1.2.2 for CI stability and switch image loading to jimp --- .github/workflows/build-core.yml | 3 +- .github/workflows/build-solid.yml | 3 +- bun.lock | 84 ++++--------------------------- packages/core/package.json | 1 - packages/core/src/renderer.ts | 28 ++++++++--- 5 files changed, 36 insertions(+), 83 deletions(-) diff --git a/.github/workflows/build-core.yml b/.github/workflows/build-core.yml index 6464254a2..b70a89b97 100644 --- a/.github/workflows/build-core.yml +++ b/.github/workflows/build-core.yml @@ -16,7 +16,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + # Latest Bun (1.3.x) intermittently crashes after tests complete; pin to a stable release + bun-version: 1.2.2 - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 diff --git a/.github/workflows/build-solid.yml b/.github/workflows/build-solid.yml index a606f2264..ccde9b077 100644 --- a/.github/workflows/build-solid.yml +++ b/.github/workflows/build-solid.yml @@ -16,7 +16,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + # Latest Bun (1.3.x) intermittently crashes after tests complete; pin to a stable release + bun-version: 1.2.2 - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 diff --git a/bun.lock b/bun.lock index fda1f8cfb..71ba5d9b4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@opentui", @@ -14,7 +15,6 @@ "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", - "sharp": "0.33.5", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -161,46 +161,6 @@ "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], @@ -267,17 +227,17 @@ "@opentui/core": ["@opentui/core@workspace:packages/core"], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.57", "", { "os": "darwin", "cpu": "arm64" }, "sha512-by+Pvh5aKw13zSuNbwQKAthrlCpdI7eU8HuIEN/PPQdHuQivtgxkMn9jgLEwIMkOnrvZ8SqdFb392zZRgSCNDg=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.58", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zlOdbzzFwNMqBqiWCvzDC/VMs1Gsjgp7Wogoh3bZYYxYyjpCVGcEjP38hY7Db36HezYhPiUnlgg9hK9Tby7hHQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.57", "", { "os": "darwin", "cpu": "x64" }, "sha512-Upcem+gU4I2/znG7IoR3l7VklZTmhxD2E4QV98sY/FAz4blD+ng/n4M3fCj+M03ZnSkoulELkPzWXH0AwIkcyw=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.58", "", { "os": "darwin", "cpu": "x64" }, "sha512-GBLbF4rvDToZoLTIV5GTWElXulAdirz52YOrfEOMkpub2DTXaXidqyJTVrNrbc+qq4ca8jzYmTeZTnZ9pPd6NQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.57", "", { "os": "linux", "cpu": "arm64" }, "sha512-0YAqzuKNLEm+NBQVSni1/pd8960Ybf8LMw5Ead5m4BNtPzYQ5QkdUEAIhsdViQFnFlCPo9AI12oRBzurGMgSmw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.58", "", { "os": "linux", "cpu": "arm64" }, "sha512-sVv6PbOxZsZLqSSwJ9R51SQaurvRUzoGJslGWickpxk5QaGHDDxj01H4gR8LfVsRK0HeEdLGvPZLFI7csRn/5A=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.57", "", { "os": "linux", "cpu": "x64" }, "sha512-2JTGolyVbuKY+DcE7V15DQwjOLygCHFgFzYwrsLCGxoaEgnEAcocWxAF0CdSlvx01/eY4u8uspzqOT94XTUJRQ=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.58", "", { "os": "linux", "cpu": "x64" }, "sha512-7fM7V1SKY3iYa1kDMmOHaDskziOzFFnEzkHcfuoLBK4qxJX196GNfH3xi5URQl/JBhavzHqctmv4KvMPOE5PMQ=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.57", "", { "os": "win32", "cpu": "arm64" }, "sha512-HPT9fPzOPaLZ2P35Ed6N1icWDgLHpzxnqKsBfDG3bZq5ClhfHGuXm00JmekxauIhR6zTTYd2WSfMCB2qpCufrw=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.58", "", { "os": "win32", "cpu": "arm64" }, "sha512-oUzb43lv8dx0oijLE2WItciAcE4+c+p78Cl922T3UDyYXUqNP0r3sZDV+Fo3j8XrgpmRWXBj1s7P5igIEGjMUw=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.57", "", { "os": "win32", "cpu": "x64" }, "sha512-Q7PcfqQjlz3idOBns3KbKMrTm15Zx4SMtReNX8BTbVP1UsLDAtIif+1JqjPx1YFRq5WCCsuRAggJaUEmQp5Nqw=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.58", "", { "os": "win32", "cpu": "x64" }, "sha512-oxx55mF6pbC9ARqy4gAmchqFXvCLsEOGQmwMrF5xS8KsBGp5As9tB1ZEYRa6zqiPhAHyp/dAv5OgO6Xvcy2Hew=="], "@opentui/react": ["@opentui/react@workspace:packages/react"], @@ -299,7 +259,7 @@ "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="], "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], @@ -373,14 +333,6 @@ "caniuse-lite": ["caniuse-lite@1.0.30001737", "", {}, "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -389,8 +341,6 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], "electron-to-chromium": ["electron-to-chromium@1.5.211", "", {}, "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw=="], @@ -433,8 +383,6 @@ "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], - "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], @@ -529,16 +477,12 @@ "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ=="], - "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], - - "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], - "simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="], "solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="], @@ -559,8 +503,6 @@ "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -587,12 +529,6 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@opentui/react/@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@opentui/react/@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], @@ -605,6 +541,8 @@ "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], diff --git a/packages/core/package.json b/packages/core/package.json index ec315c113..c78a43802 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,6 @@ "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", - "sharp": "0.33.5", "yoga-layout": "3.2.1" }, "peerDependencies": { diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 3b33cd963..80330e4a6 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -1819,14 +1819,28 @@ export class CliRenderer extends EventEmitter implements RenderContext { private async loadImage(src: string | Buffer, width: number, height: number, fit: ImageFit): Promise { try { - const sharpModule: unknown = await import("sharp") - const moduleCandidate = sharpModule as { default?: unknown } - const sharp = - typeof moduleCandidate.default === "function" - ? (moduleCandidate.default as typeof import("sharp")) - : (sharpModule as typeof import("sharp")) + const jimpModule: unknown = await import("jimp") + const Jimp = + typeof (jimpModule as any).default === "function" + ? ((jimpModule as any).default as typeof import("jimp").default) + : (jimpModule as typeof import("jimp")) const input = typeof src === "string" ? src : Buffer.from(src) - return await sharp(input).resize({ width, height, fit }).png().toBuffer() + const image = await Jimp.read(input) + + switch (fit) { + case "cover": + image.cover(width, height) + break + case "fill": + image.resize(width, height) + break + case "contain": + default: + image.contain(width, height) + break + } + + return await image.getBuffer("image/png") } catch (error) { console.error("Failed to load image", error) return null From 2e9ca164221b166b9c49b691f34d5b2028601019 Mon Sep 17 00:00:00 2001 From: "Andrew C. Oliver" Date: Mon, 8 Dec 2025 20:17:23 -0300 Subject: [PATCH 6/7] Use latest Bun with serial test concurrency for CI --- .github/workflows/build-core.yml | 7 +++++-- .github/workflows/build-solid.yml | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-core.yml b/.github/workflows/build-core.yml index b70a89b97..f8822dfae 100644 --- a/.github/workflows/build-core.yml +++ b/.github/workflows/build-core.yml @@ -16,8 +16,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - # Latest Bun (1.3.x) intermittently crashes after tests complete; pin to a stable release - bun-version: 1.2.2 + bun-version: latest - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 @@ -28,11 +27,15 @@ jobs: run: bun install - name: Build + env: + BUN_TEST_CONCURRENCY: 1 run: | cd packages/core bun run build - name: Run tests + env: + BUN_TEST_CONCURRENCY: 1 run: | cd packages/core bun run test diff --git a/.github/workflows/build-solid.yml b/.github/workflows/build-solid.yml index ccde9b077..ce68c362b 100644 --- a/.github/workflows/build-solid.yml +++ b/.github/workflows/build-solid.yml @@ -16,8 +16,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - # Latest Bun (1.3.x) intermittently crashes after tests complete; pin to a stable release - bun-version: 1.2.2 + bun-version: latest - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 @@ -28,16 +27,22 @@ jobs: run: bun install - name: Build core + env: + BUN_TEST_CONCURRENCY: 1 run: | cd packages/core bun run build - name: Build + env: + BUN_TEST_CONCURRENCY: 1 run: | cd packages/solid bun run build --ci - name: Run tests + env: + BUN_TEST_CONCURRENCY: 1 run: | cd packages/solid bun run test From 23f0ef96dbebd5f8a9814029039539410043fcd4 Mon Sep 17 00:00:00 2001 From: "Andrew C. Oliver" Date: Mon, 8 Dec 2025 20:49:13 -0300 Subject: [PATCH 7/7] Fix Jimp import for TS declarations --- packages/core/src/renderer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 80330e4a6..5e14bf1fd 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -1821,9 +1821,9 @@ export class CliRenderer extends EventEmitter implements RenderContext { try { const jimpModule: unknown = await import("jimp") const Jimp = - typeof (jimpModule as any).default === "function" - ? ((jimpModule as any).default as typeof import("jimp").default) - : (jimpModule as typeof import("jimp")) + (jimpModule as { Jimp?: any }).Jimp ?? + (jimpModule as { default?: any }).default ?? + (jimpModule as any) const input = typeof src === "string" ? src : Buffer.from(src) const image = await Jimp.read(input)